diff --git a/.config/hakari.toml b/.config/hakari.toml index 8ce0b77490..2050065cc2 100644 --- a/.config/hakari.toml +++ b/.config/hakari.toml @@ -25,8 +25,6 @@ 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" }, ] [final-excludes] @@ -34,6 +32,7 @@ workspace-members = [ "zed_extension_api", # exclude all extensions + "zed_emmet", "zed_glsl", "zed_html", "zed_proto", diff --git a/.github/ISSUE_TEMPLATE/07_bug_windows_alpha.yml b/.github/ISSUE_TEMPLATE/07_bug_windows_alpha.yml deleted file mode 100644 index 826c2b8027..0000000000 --- a/.github/ISSUE_TEMPLATE/07_bug_windows_alpha.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Bug Report (Windows Alpha) -description: Zed Windows Alpha Related Bugs -type: "Bug" -labels: ["windows"] -title: "Windows Alpha: " -body: - - type: textarea - attributes: - label: Summary - description: Describe the bug with a one-line summary, and provide detailed reproduction steps - value: | - - SUMMARY_SENTENCE_HERE - - ### Description - - Steps to trigger the problem: - 1. - 2. - 3. - - **Expected Behavior**: - **Actual Behavior**: - - validations: - required: true - - type: textarea - id: environment - attributes: - label: Zed Version and System Specs - description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"' - placeholder: | - Output of "zed: copy system specs into clipboard" - validations: - required: true diff --git a/.github/ISSUE_TEMPLATE/10_bug_report.yml b/.github/ISSUE_TEMPLATE/10_bug_report.yml index 1bf6c80e40..e132eca1e5 100644 --- a/.github/ISSUE_TEMPLATE/10_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/10_bug_report.yml @@ -14,7 +14,7 @@ body: ### Description diff --git a/.github/actionlint.yml b/.github/actionlint.yml index 6d8e0107e9..d93ec5b15e 100644 --- a/.github/actionlint.yml +++ b/.github/actionlint.yml @@ -5,41 +5,26 @@ self-hosted-runner: # GitHub-hosted Runners - github-8vcpu-ubuntu-2404 - github-16vcpu-ubuntu-2404 - - github-32vcpu-ubuntu-2404 - - github-8vcpu-ubuntu-2204 - - github-16vcpu-ubuntu-2204 - - github-32vcpu-ubuntu-2204 - - github-16vcpu-ubuntu-2204-arm - windows-2025-16 - windows-2025-32 - windows-2025-64 - # Namespace Ubuntu 20.04 (Release builds) - - namespace-profile-16x32-ubuntu-2004 - - namespace-profile-32x64-ubuntu-2004 - - namespace-profile-16x32-ubuntu-2004-arm - - namespace-profile-32x64-ubuntu-2004-arm - # Namespace Ubuntu 22.04 (Everything else) - - 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 + # Buildjet Ubuntu 20.04 - AMD x86_64 + - buildjet-2vcpu-ubuntu-2004 + - buildjet-4vcpu-ubuntu-2004 + - buildjet-8vcpu-ubuntu-2004 + - buildjet-16vcpu-ubuntu-2004 + - buildjet-32vcpu-ubuntu-2004 + # Buildjet Ubuntu 22.04 - AMD x86_64 + - buildjet-2vcpu-ubuntu-2204 + - buildjet-4vcpu-ubuntu-2204 + - buildjet-8vcpu-ubuntu-2204 + - buildjet-16vcpu-ubuntu-2204 + - buildjet-32vcpu-ubuntu-2204 + # Buildjet Ubuntu 22.04 - Graviton aarch64 + - buildjet-8vcpu-ubuntu-2204-arm + - buildjet-16vcpu-ubuntu-2204-arm + - buildjet-32vcpu-ubuntu-2204-arm + - buildjet-64vcpu-ubuntu-2204-arm # 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" diff --git a/.github/actions/build_docs/action.yml b/.github/actions/build_docs/action.yml index d2e62d5b22..a7effad247 100644 --- a/.github/actions/build_docs/action.yml +++ b/.github/actions/build_docs/action.yml @@ -13,7 +13,7 @@ runs: uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - # cache-provider: "buildjet" + cache-provider: "buildjet" - name: Install Linux dependencies shell: bash -euxo pipefail {0} diff --git a/.github/actions/run_tests_windows/action.yml b/.github/actions/run_tests_windows/action.yml index 0a550c7d32..cbe95e82c1 100644 --- a/.github/actions/run_tests_windows/action.yml +++ b/.github/actions/run_tests_windows/action.yml @@ -20,167 +20,7 @@ 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 - - - 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 - } + run: cargo nextest run --workspace --no-fail-fast diff --git a/.github/workflows/bump_collab_staging.yml b/.github/workflows/bump_collab_staging.yml index d400905b4d..d8eaa6019e 100644 --- a/.github/workflows/bump_collab_staging.yml +++ b/.github/workflows/bump_collab_staging.yml @@ -8,7 +8,7 @@ on: jobs: update-collab-staging-tag: if: github.repository_owner == 'zed-industries' - runs-on: namespace-profile-2x4-ubuntu-2404 + runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 diff --git a/.github/workflows/bump_patch_version.yml b/.github/workflows/bump_patch_version.yml index bfaf7a271b..8a48ff96f1 100644 --- a/.github/workflows/bump_patch_version.yml +++ b/.github/workflows/bump_patch_version.yml @@ -16,7 +16,7 @@ jobs: bump_patch_version: if: github.repository_owner == 'zed-industries' runs-on: - - namespace-profile-16x32-ubuntu-2204 + - buildjet-16vcpu-ubuntu-2204 steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a34833d0fd..7dfc33e0d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,6 @@ env: DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }} DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }} ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} - ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }} jobs: job_spec: @@ -37,7 +36,7 @@ jobs: run_nix: ${{ steps.filter.outputs.run_nix }} run_actionlint: ${{ steps.filter.outputs.run_actionlint }} runs-on: - - namespace-profile-2x4-ubuntu-2404 + - ubuntu-latest steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 @@ -137,7 +136,7 @@ jobs: github.repository_owner == 'zed-industries' && needs.job_spec.outputs.run_tests == 'true' runs-on: - - namespace-profile-8x16-ubuntu-2204 + - buildjet-8vcpu-ubuntu-2204 steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 @@ -168,7 +167,7 @@ jobs: needs: [job_spec] if: github.repository_owner == 'zed-industries' runs-on: - - namespace-profile-4x8-ubuntu-2204 + - buildjet-8vcpu-ubuntu-2204 steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 @@ -221,7 +220,7 @@ jobs: github.repository_owner == 'zed-industries' && (needs.job_spec.outputs.run_tests == 'true' || needs.job_spec.outputs.run_docs == 'true') runs-on: - - namespace-profile-8x16-ubuntu-2204 + - buildjet-8vcpu-ubuntu-2204 steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 @@ -237,7 +236,7 @@ jobs: uses: ./.github/actions/build_docs actionlint: - runs-on: namespace-profile-2x4-ubuntu-2404 + runs-on: ubuntu-latest if: github.repository_owner == 'zed-industries' && needs.job_spec.outputs.run_actionlint == 'true' needs: [job_spec] steps: @@ -328,7 +327,7 @@ jobs: github.repository_owner == 'zed-industries' && needs.job_spec.outputs.run_tests == 'true' runs-on: - - namespace-profile-16x32-ubuntu-2204 + - buildjet-16vcpu-ubuntu-2204 steps: - name: Add Rust to the PATH run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" @@ -342,7 +341,7 @@ jobs: uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - # cache-provider: "buildjet" + cache-provider: "buildjet" - name: Install Linux dependencies run: ./script/linux @@ -380,7 +379,7 @@ jobs: github.repository_owner == 'zed-industries' && needs.job_spec.outputs.run_tests == 'true' runs-on: - - namespace-profile-16x32-ubuntu-2204 + - buildjet-8vcpu-ubuntu-2204 steps: - name: Add Rust to the PATH run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" @@ -394,7 +393,7 @@ jobs: uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - # cache-provider: "buildjet" + cache-provider: "buildjet" - name: Install Clang & Mold run: ./script/remote-server && ./script/install-mold 2.34.0 @@ -418,7 +417,7 @@ jobs: if: | github.repository_owner == 'zed-industries' && needs.job_spec.outputs.run_tests == 'true' - runs-on: [self-32vcpu-windows-2022] + runs-on: [self-hosted, Windows, X64] steps: - name: Environment Setup run: | @@ -458,7 +457,7 @@ jobs: tests_pass: name: Tests Pass - runs-on: namespace-profile-2x4-ubuntu-2404 + runs-on: ubuntu-latest needs: - job_spec - style @@ -511,8 +510,8 @@ jobs: runs-on: - self-mini-macos if: | - ( startsWith(github.ref, 'refs/tags/v') - || contains(github.event.pull_request.labels.*.name, 'run-bundling') ) + startsWith(github.ref, 'refs/tags/v') + || contains(github.event.pull_request.labels.*.name, 'run-bundling') needs: [macos_tests] env: MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} @@ -526,11 +525,6 @@ jobs: with: node-version: "18" - - name: Setup Sentry CLI - uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2 - with: - token: ${{ SECRETS.SENTRY_AUTH_TOKEN }} - - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: @@ -602,10 +596,10 @@ jobs: timeout-minutes: 60 name: Linux x86_x64 release bundle runs-on: - - namespace-profile-16x32-ubuntu-2004 # ubuntu 20.04 for minimal glibc + - buildjet-16vcpu-ubuntu-2004 # ubuntu 20.04 for minimal glibc if: | - ( startsWith(github.ref, 'refs/tags/v') - || contains(github.event.pull_request.labels.*.name, 'run-bundling') ) + startsWith(github.ref, 'refs/tags/v') + || contains(github.event.pull_request.labels.*.name, 'run-bundling') needs: [linux_tests] steps: - name: Checkout repo @@ -616,11 +610,6 @@ jobs: - name: Install Linux dependencies run: ./script/linux && ./script/install-mold 2.34.0 - - name: Setup Sentry CLI - uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2 - with: - token: ${{ SECRETS.SENTRY_AUTH_TOKEN }} - - name: Determine version and release channel if: startsWith(github.ref, 'refs/tags/v') run: | @@ -660,7 +649,7 @@ jobs: timeout-minutes: 60 name: Linux arm64 release bundle runs-on: - - namespace-profile-8x32-ubuntu-2004-arm-m4 # ubuntu 20.04 for minimal glibc + - buildjet-16vcpu-ubuntu-2204-arm if: | startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') @@ -674,11 +663,6 @@ jobs: - name: Install Linux dependencies run: ./script/linux - - name: Setup Sentry CLI - uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2 - with: - token: ${{ SECRETS.SENTRY_AUTH_TOKEN }} - - name: Determine version and release channel if: startsWith(github.ref, 'refs/tags/v') run: | @@ -718,8 +702,10 @@ jobs: timeout-minutes: 60 runs-on: github-8vcpu-ubuntu-2404 if: | - false && ( startsWith(github.ref, 'refs/tags/v') - || contains(github.event.pull_request.labels.*.name, 'run-bundling') ) + false && ( + startsWith(github.ref, 'refs/tags/v') + || contains(github.event.pull_request.labels.*.name, 'run-bundling') + ) needs: [linux_tests] name: Build Zed on FreeBSD steps: @@ -784,7 +770,7 @@ jobs: bundle-windows-x64: timeout-minutes: 120 name: Create a Windows installer - runs-on: [self-32vcpu-windows-2022] + runs-on: [self-hosted, Windows, X64] 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] @@ -804,11 +790,6 @@ jobs: with: clean: false - - name: Setup Sentry CLI - uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2 - with: - token: ${{ SECRETS.SENTRY_AUTH_TOKEN }} - - name: Determine version and release channel working-directory: ${{ env.ZED_WORKSPACE }} if: ${{ startsWith(github.ref, 'refs/tags/v') }} @@ -851,12 +832,3 @@ jobs: run: gh release edit "$GITHUB_REF_NAME" --draft=false env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Create Sentry release - uses: getsentry/action-release@526942b68292201ac6bbb99b9a0747d4abee354c # v3 - env: - SENTRY_ORG: zed-dev - SENTRY_PROJECT: zed - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - with: - environment: production diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml index 3f84179278..15c82643ae 100644 --- a/.github/workflows/danger.yml +++ b/.github/workflows/danger.yml @@ -12,7 +12,7 @@ on: jobs: danger: if: github.repository_owner == 'zed-industries' - runs-on: namespace-profile-2x4-ubuntu-2404 + runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 diff --git a/.github/workflows/deploy_cloudflare.yml b/.github/workflows/deploy_cloudflare.yml index df35d44ca9..fe443d493e 100644 --- a/.github/workflows/deploy_cloudflare.yml +++ b/.github/workflows/deploy_cloudflare.yml @@ -9,7 +9,7 @@ jobs: deploy-docs: name: Deploy Docs if: github.repository_owner == 'zed-industries' - runs-on: namespace-profile-16x32-ubuntu-2204 + runs-on: buildjet-16vcpu-ubuntu-2204 steps: - name: Checkout repo diff --git a/.github/workflows/deploy_collab.yml b/.github/workflows/deploy_collab.yml index ff2a3589e4..f7348a1069 100644 --- a/.github/workflows/deploy_collab.yml +++ b/.github/workflows/deploy_collab.yml @@ -61,7 +61,7 @@ jobs: - style - tests runs-on: - - namespace-profile-16x32-ubuntu-2204 + - buildjet-16vcpu-ubuntu-2204 steps: - name: Install doctl uses: digitalocean/action-doctl@v2 @@ -94,7 +94,7 @@ jobs: needs: - publish runs-on: - - namespace-profile-16x32-ubuntu-2204 + - buildjet-16vcpu-ubuntu-2204 steps: - name: Checkout repo @@ -137,14 +137,12 @@ jobs: export ZED_SERVICE_NAME=collab export ZED_LOAD_BALANCER_SIZE_UNIT=$ZED_COLLAB_LOAD_BALANCER_SIZE_UNIT - export DATABASE_MAX_CONNECTIONS=850 envsubst < crates/collab/k8s/collab.template.yml | kubectl apply -f - kubectl -n "$ZED_KUBE_NAMESPACE" rollout status deployment/$ZED_SERVICE_NAME --watch echo "deployed ${ZED_SERVICE_NAME} to ${ZED_KUBE_NAMESPACE}" export ZED_SERVICE_NAME=api export ZED_LOAD_BALANCER_SIZE_UNIT=$ZED_API_LOAD_BALANCER_SIZE_UNIT - export DATABASE_MAX_CONNECTIONS=60 envsubst < crates/collab/k8s/collab.template.yml | kubectl apply -f - kubectl -n "$ZED_KUBE_NAMESPACE" rollout status deployment/$ZED_SERVICE_NAME --watch echo "deployed ${ZED_SERVICE_NAME} to ${ZED_KUBE_NAMESPACE}" diff --git a/.github/workflows/eval.yml b/.github/workflows/eval.yml index b5da9e7b7c..2ad302a602 100644 --- a/.github/workflows/eval.yml +++ b/.github/workflows/eval.yml @@ -32,7 +32,7 @@ jobs: github.repository_owner == 'zed-industries' && (github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-eval')) runs-on: - - namespace-profile-16x32-ubuntu-2204 + - buildjet-16vcpu-ubuntu-2204 steps: - name: Add Rust to the PATH run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" @@ -46,7 +46,7 @@ jobs: uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - # cache-provider: "buildjet" + cache-provider: "buildjet" - name: Install Linux dependencies run: ./script/linux diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index e682ce5890..beacd27774 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -20,7 +20,7 @@ jobs: matrix: system: - os: x86 Linux - runner: namespace-profile-16x32-ubuntu-2204 + runner: buildjet-16vcpu-ubuntu-2204 install_nix: true - os: arm Mac runner: [macOS, ARM64, test] @@ -29,7 +29,6 @@ jobs: runs-on: ${{ matrix.system.runner }} env: ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} - ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }} ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }} GIT_LFS_SKIP_SMUDGE: 1 # breaks the livekit rust sdk examples which we don't actually depend on steps: diff --git a/.github/workflows/randomized_tests.yml b/.github/workflows/randomized_tests.yml index de96c3df78..db4d44318e 100644 --- a/.github/workflows/randomized_tests.yml +++ b/.github/workflows/randomized_tests.yml @@ -20,7 +20,7 @@ jobs: name: Run randomized tests if: github.repository_owner == 'zed-industries' runs-on: - - namespace-profile-16x32-ubuntu-2204 + - buildjet-16vcpu-ubuntu-2204 steps: - name: Install Node uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index 2026ee7b73..4f7506967b 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -13,7 +13,6 @@ env: CARGO_INCREMENTAL: 0 RUST_BACKTRACE: 1 ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} - ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }} DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }} DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }} @@ -59,7 +58,7 @@ jobs: timeout-minutes: 60 name: Run tests on Windows if: github.repository_owner == 'zed-industries' - runs-on: [self-32vcpu-windows-2022] + runs-on: [self-hosted, Windows, X64] steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 @@ -128,7 +127,7 @@ jobs: name: Create a Linux *.tar.gz bundle for x86 if: github.repository_owner == 'zed-industries' runs-on: - - namespace-profile-16x32-ubuntu-2004 # ubuntu 20.04 for minimal glibc + - buildjet-16vcpu-ubuntu-2004 needs: tests steps: - name: Checkout repo @@ -168,7 +167,7 @@ jobs: name: Create a Linux *.tar.gz bundle for ARM if: github.repository_owner == 'zed-industries' runs-on: - - namespace-profile-8x32-ubuntu-2004-arm-m4 # ubuntu 20.04 for minimal glibc + - buildjet-16vcpu-ubuntu-2204-arm needs: tests steps: - name: Checkout repo @@ -206,6 +205,9 @@ 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 @@ -240,6 +242,7 @@ jobs: bundle-nix: name: Build and cache Nix package + if: false needs: tests secrets: inherit uses: ./.github/workflows/nix.yml @@ -248,7 +251,7 @@ jobs: timeout-minutes: 60 name: Create a Windows installer if: github.repository_owner == 'zed-industries' - runs-on: [self-32vcpu-windows-2022] + runs-on: [self-hosted, Windows, X64] needs: windows-tests env: AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }} @@ -290,7 +293,7 @@ jobs: update-nightly-tag: name: Update nightly tag if: github.repository_owner == 'zed-industries' - runs-on: namespace-profile-2x4-ubuntu-2404 + runs-on: ubuntu-latest needs: - bundle-mac - bundle-linux-x86 @@ -312,12 +315,3 @@ jobs: git config user.email github-actions@github.com git tag -f nightly git push origin nightly --force - - - name: Create Sentry release - uses: getsentry/action-release@526942b68292201ac6bbb99b9a0747d4abee354c # v3 - env: - SENTRY_ORG: zed-dev - SENTRY_PROJECT: zed - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - with: - environment: production diff --git a/.github/workflows/script_checks.yml b/.github/workflows/script_checks.yml index 5dbfc9cb7f..c32a433e46 100644 --- a/.github/workflows/script_checks.yml +++ b/.github/workflows/script_checks.yml @@ -12,7 +12,7 @@ jobs: shellcheck: name: "ShellCheck Scripts" if: github.repository_owner == 'zed-industries' - runs-on: namespace-profile-2x4-ubuntu-2404 + runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 diff --git a/.github/workflows/unit_evals.yml b/.github/workflows/unit_evals.yml index c03cf8b087..cb4e39d151 100644 --- a/.github/workflows/unit_evals.yml +++ b/.github/workflows/unit_evals.yml @@ -3,7 +3,7 @@ name: Run Unit Evals on: schedule: # GitHub might drop jobs at busy times, so we choose a random time in the middle of the night. - - cron: "47 1 * * 2" + - cron: "47 1 * * *" workflow_dispatch: concurrency: @@ -23,7 +23,7 @@ jobs: timeout-minutes: 60 name: Run unit evals runs-on: - - namespace-profile-16x32-ubuntu-2204 + - buildjet-16vcpu-ubuntu-2204 steps: - name: Add Rust to the PATH run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" @@ -37,7 +37,7 @@ jobs: uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2 with: save-if: ${{ github.ref == 'refs/heads/main' }} - # cache-provider: "buildjet" + cache-provider: "buildjet" - name: Install Linux dependencies run: ./script/linux diff --git a/Cargo.lock b/Cargo.lock index 42649b137f..8a7d512b83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6,85 +6,31 @@ version = 4 name = "acp_thread" version = "0.1.0" dependencies = [ - "action_log", "agent-client-protocol", + "agentic-coding-protocol", "anyhow", + "assistant_tool", + "async-pipe", "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", "project", - "prompt_store", - "rand 0.8.5", "serde", "serde_json", "settings", "smol", "tempfile", - "terminal", - "ui", - "url", - "util", - "uuid", - "watch", - "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", ] -[[package]] -name = "action_log" -version = "0.1.0" -dependencies = [ - "anyhow", - "buffer_diff", - "clock", - "collections", - "ctor", - "futures 0.3.31", - "gpui", - "indoc", - "language", - "log", - "pretty_assertions", - "project", - "rand 0.8.5", - "serde_json", - "settings", - "text", - "util", - "watch", - "workspace-hack", - "zlog", -] - [[package]] name = "activity_indicator" version = "0.1.0" @@ -137,7 +83,6 @@ dependencies = [ name = "agent" version = "0.1.0" dependencies = [ - "action_log", "agent_settings", "anyhow", "assistant_context", @@ -150,6 +95,7 @@ dependencies = [ "component", "context_server", "convert_case 0.8.0", + "feature_flags", "fs", "futures 0.3.31", "git", @@ -168,6 +114,7 @@ dependencies = [ "pretty_assertions", "project", "prompt_store", + "proto", "rand 0.8.5", "ref-cast", "rope", @@ -191,134 +138,44 @@ dependencies = [ [[package]] name = "agent-client-protocol" -version = "0.0.31" +version = "0.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "289eb34ee17213dadcca47eedadd386a5e7678094095414e475965d1bcca2860" +checksum = "72ec54650c1fc2d63498bab47eeeaa9eddc7d239d53f615b797a0e84f7ccc87b" dependencies = [ - "anyhow", - "async-broadcast", - "futures 0.3.31", - "log", - "parking_lot", "schemars", "serde", "serde_json", ] -[[package]] -name = "agent2" -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", - "client", - "clock", - "cloud_llm_client", - "collections", - "context_server", - "ctor", - "db", - "editor", - "env_logger 0.11.8", - "fs", - "futures 0.3.31", - "git", - "gpui", - "gpui_tokio", - "handlebars 4.5.0", - "html_to_markdown", - "http_client", - "indoc", - "itertools 0.14.0", - "language", - "language_model", - "language_models", - "log", - "lsp", - "open", - "parking_lot", - "paths", - "portable-pty", - "pretty_assertions", - "project", - "prompt_store", - "reqwest_client", - "rust-embed", - "schemars", - "serde", - "serde_json", - "settings", - "smol", - "sqlez", - "task", - "telemetry", - "tempfile", - "terminal", - "text", - "theme", - "thiserror 2.0.12", - "tree-sitter-rust", - "ui", - "unindent", - "util", - "uuid", - "watch", - "web_search", - "which 6.0.3", - "workspace-hack", - "worktree", - "zlog", - "zstd", -] - [[package]] name = "agent_servers" version = "0.1.0" dependencies = [ "acp_thread", - "acp_tools", - "action_log", "agent-client-protocol", - "agent_settings", + "agentic-coding-protocol", "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", - "reqwest_client", "schemars", - "semver", "serde", "serde_json", "settings", "smol", "strum 0.27.1", "tempfile", - "thiserror 2.0.12", "ui", "util", "uuid", @@ -351,10 +208,8 @@ name = "agent_ui" version = "0.1.0" dependencies = [ "acp_thread", - "action_log", "agent", "agent-client-protocol", - "agent2", "agent_servers", "agent_settings", "ai_onboarding", @@ -385,6 +240,7 @@ dependencies = [ "gpui", "html_to_markdown", "http_client", + "indexed_docs", "indoc", "inventory", "itertools 0.14.0", @@ -403,7 +259,6 @@ dependencies = [ "parking_lot", "paths", "picker", - "postage", "pretty_assertions", "project", "prompt_store", @@ -433,7 +288,6 @@ dependencies = [ "ui", "ui_input", "unindent", - "url", "urlencoding", "util", "uuid", @@ -443,6 +297,24 @@ 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" @@ -487,6 +359,7 @@ dependencies = [ "component", "gpui", "language_model", + "proto", "serde", "smallvec", "telemetry", @@ -858,7 +731,7 @@ dependencies = [ "anyhow", "async-trait", "collections", - "derive_more", + "derive_more 0.99.19", "extension", "futures 0.3.31", "gpui", @@ -892,6 +765,7 @@ dependencies = [ "gpui", "html_to_markdown", "http_client", + "indexed_docs", "language", "pretty_assertions", "project", @@ -915,13 +789,13 @@ dependencies = [ name = "assistant_tool" version = "0.1.0" dependencies = [ - "action_log", "anyhow", "buffer_diff", "clock", "collections", "ctor", - "derive_more", + "derive_more 0.99.19", + "futures 0.3.31", "gpui", "icons", "indoc", @@ -938,6 +812,7 @@ dependencies = [ "settings", "text", "util", + "watch", "workspace", "workspace-hack", "zlog", @@ -947,7 +822,6 @@ dependencies = [ name = "assistant_tools" version = "0.1.0" dependencies = [ - "action_log", "agent_settings", "anyhow", "assistant_tool", @@ -958,7 +832,7 @@ dependencies = [ "cloud_llm_client", "collections", "component", - "derive_more", + "derive_more 0.99.19", "diffy", "editor", "feature_flags", @@ -1202,6 +1076,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "async-recursion" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d78656ba01f1b93024b7c3a0467f1608e4be67d725749fdcd7d2c7678fd7a2" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "async-recursion" version = "1.1.1" @@ -1281,6 +1166,26 @@ 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", + "smol_str 0.1.24", + "thiserror 1.0.69", + "tokio", +] + [[package]] name = "async-tar" version = "0.5.0" @@ -1303,9 +1208,9 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.89" +version = "0.1.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", @@ -1384,11 +1289,10 @@ version = "0.1.0" dependencies = [ "anyhow", "collections", + "derive_more 0.99.19", "gpui", + "parking_lot", "rodio", - "schemars", - "serde", - "settings", "util", "workspace-hack", ] @@ -1475,7 +1379,7 @@ dependencies = [ "anyhow", "arrayvec", "log", - "nom 7.1.3", + "nom", "num-rational", "v_frame", ] @@ -2083,6 +1987,12 @@ 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" @@ -2843,7 +2753,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ - "nom 7.1.3", + "nom", ] [[package]] @@ -3062,6 +2972,7 @@ name = "client" version = "0.1.0" dependencies = [ "anyhow", + "async-recursion 0.3.2", "async-tungstenite", "base64 0.22.1", "chrono", @@ -3071,7 +2982,7 @@ dependencies = [ "cocoa 0.26.0", "collections", "credentials_provider", - "derive_more", + "derive_more 0.99.19", "feature_flags", "fs", "futures 0.3.31", @@ -3092,7 +3003,6 @@ dependencies = [ "schemars", "serde", "serde_json", - "serde_urlencoded", "settings", "sha2", "smol", @@ -3130,22 +3040,17 @@ dependencies = [ "anyhow", "cloud_api_types", "futures 0.3.31", - "gpui", - "gpui_tokio", "http_client", "parking_lot", "serde_json", "workspace-hack", - "yawc", ] [[package]] name = "cloud_api_types" version = "0.1.0" dependencies = [ - "anyhow", "chrono", - "ciborium", "cloud_llm_client", "pretty_assertions", "serde", @@ -3276,6 +3181,7 @@ dependencies = [ "anyhow", "assistant_context", "assistant_slash_command", + "async-stripe", "async-trait", "async-tungstenite", "audio", @@ -3291,6 +3197,7 @@ dependencies = [ "chrono", "client", "clock", + "cloud_llm_client", "collab_ui", "collections", "command_palette_hooks", @@ -3301,6 +3208,7 @@ dependencies = [ "dap_adapters", "dashmap 6.1.0", "debugger_ui", + "derive_more 0.99.19", "editor", "envy", "extension", @@ -3316,6 +3224,7 @@ dependencies = [ "http_client", "hyper 0.14.32", "indoc", + "jsonwebtoken", "language", "language_model", "livekit_api", @@ -3361,6 +3270,7 @@ dependencies = [ "telemetry_events", "text", "theme", + "thiserror 2.0.12", "time", "tokio", "toml 0.8.20", @@ -3503,7 +3413,7 @@ name = "command_palette_hooks" version = "0.1.0" dependencies = [ "collections", - "derive_more", + "derive_more 0.99.19", "gpui", "workspace-hack", ] @@ -3641,13 +3551,13 @@ dependencies = [ "command_palette_hooks", "ctor", "dirs 4.0.0", - "edit_prediction", "editor", "fs", "futures 0.3.31", "gpui", "http_client", "indoc", + "inline_completion", "itertools 0.14.0", "language", "log", @@ -3661,7 +3571,6 @@ dependencies = [ "serde", "serde_json", "settings", - "sum_tree", "task", "theme", "ui", @@ -3862,7 +3771,7 @@ dependencies = [ "rustc-hash 1.1.0", "rustybuzz 0.14.1", "self_cell", - "smol_str", + "smol_str 0.2.2", "swash", "sys-locale", "ttf-parser 0.21.1", @@ -3884,7 +3793,7 @@ dependencies = [ "jni", "js-sys", "libc", - "mach2 0.4.2", + "mach2", "ndk", "ndk-context", "num-derive", @@ -4026,48 +3935,6 @@ dependencies = [ "target-lexicon 0.13.2", ] -[[package]] -name = "crash-context" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "031ed29858d90cfdf27fe49fae28028a1f20466db97962fa2f4ea34809aeebf3" -dependencies = [ - "cfg-if", - "libc", - "mach2 0.4.2", -] - -[[package]] -name = "crash-handler" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2066907075af649bcb8bcb1b9b986329b243677e6918b2d920aa64b0aac5ace3" -dependencies = [ - "cfg-if", - "crash-context", - "libc", - "mach2 0.4.2", - "parking_lot", -] - -[[package]] -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", -] - [[package]] name = "crc" version = "3.2.1" @@ -4594,15 +4461,6 @@ dependencies = [ "zlog", ] -[[package]] -name = "debugid" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" -dependencies = [ - "uuid", -] - [[package]] name = "deepseek" version = "0.1.0" @@ -4666,6 +4524,27 @@ 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" @@ -4686,6 +4565,7 @@ dependencies = [ "component", "ctor", "editor", + "futures 0.3.31", "gpui", "indoc", "language", @@ -4984,49 +4864,6 @@ dependencies = [ "signature 1.6.4", ] -[[package]] -name = "edit_prediction" -version = "0.1.0" -dependencies = [ - "client", - "gpui", - "language", - "project", - "workspace-hack", -] - -[[package]] -name = "edit_prediction_button" -version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "cloud_llm_client", - "copilot", - "edit_prediction", - "editor", - "feature_flags", - "fs", - "futures 0.3.31", - "gpui", - "indoc", - "language", - "lsp", - "paths", - "project", - "regex", - "serde_json", - "settings", - "supermaven", - "telemetry", - "theme", - "ui", - "workspace", - "workspace-hack", - "zed_actions", - "zeta", -] - [[package]] name = "editor" version = "0.1.0" @@ -5042,7 +4879,6 @@ dependencies = [ "ctor", "dap", "db", - "edit_prediction", "emojis", "file_icons", "fs", @@ -5052,6 +4888,7 @@ dependencies = [ "gpui", "http_client", "indoc", + "inline_completion", "itertools 0.14.0", "language", "languages", @@ -5722,10 +5559,14 @@ dependencies = [ name = "feedback" version = "0.1.0" dependencies = [ + "client", "editor", "gpui", + "human_bytes", "menu", - "system_specs", + "release_channel", + "serde", + "sysinfo", "ui", "urlencoding", "util", @@ -6345,6 +6186,17 @@ 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" @@ -6401,7 +6253,7 @@ dependencies = [ "askpass", "async-trait", "collections", - "derive_more", + "derive_more 0.99.19", "futures 0.3.31", "git2", "gpui", @@ -6409,7 +6261,6 @@ dependencies = [ "log", "parking_lot", "pretty_assertions", - "rand 0.8.5", "regex", "rope", "schemars", @@ -6473,6 +6324,7 @@ dependencies = [ "buffer_diff", "call", "chrono", + "client", "cloud_llm_client", "collections", "command_palette_hooks", @@ -6484,7 +6336,6 @@ dependencies = [ "fuzzy", "git", "gpui", - "indoc", "itertools 0.14.0", "language", "language_model", @@ -7349,17 +7200,6 @@ dependencies = [ "workspace-hack", ] -[[package]] -name = "goblin" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47" -dependencies = [ - "log", - "plain", - "scroll", -] - [[package]] name = "google_ai" version = "0.1.0" @@ -7431,7 +7271,7 @@ dependencies = [ "core-video", "cosmic-text", "ctor", - "derive_more", + "derive_more 0.99.19", "embed-resource", "env_logger 0.11.8", "etagere", @@ -7477,7 +7317,6 @@ dependencies = [ "slotmap", "smallvec", "smol", - "stacksafe", "strum 0.27.1", "sum_tree", "taffy", @@ -7519,7 +7358,6 @@ dependencies = [ name = "gpui_tokio" version = "0.1.0" dependencies = [ - "anyhow", "gpui", "tokio", "util", @@ -7528,9 +7366,9 @@ dependencies = [ [[package]] name = "grid" -version = "0.18.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12101ecc8225ea6d675bc70263074eab6169079621c2186fe0c66590b2df9681" +checksum = "71b01d27060ad58be4663b9e4ac9e2d4806918e8876af8912afbddd1a91d5eaa" [[package]] name = "group" @@ -7843,12 +7681,6 @@ 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" @@ -7950,19 +7782,38 @@ 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", + "derive_more 0.99.19", "futures 0.3.31", "http 1.3.1", "http-body 1.0.1", "log", - "parking_lot", - "reqwest 0.12.15 (git+https://github.com/zed-industries/reqwest.git?rev=951c770a32f1998d6e999cef3e59e0013e6c4415)", "serde", "serde_json", "url", @@ -8383,6 +8234,34 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" +[[package]] +name = "indexed_docs" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "cargo_metadata", + "collections", + "derive_more 0.99.19", + "extension", + "fs", + "futures 0.3.31", + "fuzzy", + "gpui", + "heed", + "html_to_markdown", + "http_client", + "indexmap", + "indoc", + "parking_lot", + "paths", + "pretty_assertions", + "serde", + "strum 0.27.1", + "util", + "workspace-hack", +] + [[package]] name = "indexmap" version = "2.9.0" @@ -8400,6 +8279,12 @@ version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" +[[package]] +name = "infer" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac" + [[package]] name = "inherent" version = "1.0.12" @@ -8411,6 +8296,49 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "inline_completion" +version = "0.1.0" +dependencies = [ + "client", + "gpui", + "language", + "project", + "workspace-hack", +] + +[[package]] +name = "inline_completion_button" +version = "0.1.0" +dependencies = [ + "anyhow", + "client", + "cloud_llm_client", + "copilot", + "editor", + "feature_flags", + "fs", + "futures 0.3.31", + "gpui", + "indoc", + "inline_completion", + "language", + "lsp", + "paths", + "project", + "regex", + "serde_json", + "settings", + "supermaven", + "telemetry", + "theme", + "ui", + "workspace", + "workspace-hack", + "zed_actions", + "zeta", +] + [[package]] name = "inotify" version = "0.9.6" @@ -8468,7 +8396,6 @@ dependencies = [ "theme", "ui", "util", - "util_macros", "workspace", "workspace-hack", "zed_actions", @@ -9101,7 +9028,6 @@ dependencies = [ "anyhow", "base64 0.22.1", "client", - "cloud_api_types", "cloud_llm_client", "collections", "futures 0.3.31", @@ -9159,6 +9085,7 @@ dependencies = [ "open_router", "partial-json-fixer", "project", + "proto", "release_channel", "schemars", "serde", @@ -9232,7 +9159,6 @@ version = "0.1.0" dependencies = [ "anyhow", "async-compression", - "async-fs", "async-tar", "async-trait", "chrono", @@ -9264,11 +9190,9 @@ dependencies = [ "serde_json", "serde_json_lenient", "settings", - "sha2", "smol", "snippet_provider", "task", - "tempfile", "text", "theme", "toml 0.8.20", @@ -9605,7 +9529,6 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", - "audio", "collections", "core-foundation 0.10.0", "core-video", @@ -9624,11 +9547,9 @@ dependencies = [ "objc", "parking_lot", "postage", - "rodio", "scap", "serde", "serde_json", - "settings", "sha2", "simplelog", "smallvec", @@ -9663,9 +9584,9 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -9859,15 +9780,6 @@ dependencies = [ "libc", ] -[[package]] -name = "mach2" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea" -dependencies = [ - "libc", -] - [[package]] name = "malloc_buf" version = "0.0.6" @@ -9911,7 +9823,7 @@ name = "markdown_preview" version = "0.1.0" dependencies = [ "anyhow", - "async-recursion", + "async-recursion 1.1.1", "collections", "editor", "fs", @@ -10176,63 +10088,6 @@ dependencies = [ "unicase", ] -[[package]] -name = "minidump-common" -version = "0.21.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c4d14bcca0fd3ed165a03000480aaa364c6860c34e900cb2dafdf3b95340e77" -dependencies = [ - "bitflags 2.9.0", - "debugid", - "num-derive", - "num-traits", - "range-map", - "scroll", - "smart-default", -] - -[[package]] -name = "minidump-writer" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abcd9c8a1e6e1e9d56ce3627851f39a17ea83e17c96bc510f29d7e43d78a7d" -dependencies = [ - "bitflags 2.9.0", - "byteorder", - "cfg-if", - "crash-context", - "goblin", - "libc", - "log", - "mach2 0.4.2", - "memmap2", - "memoffset", - "minidump-common", - "nix 0.28.0", - "procfs-core", - "scroll", - "tempfile", - "thiserror 1.0.69", -] - -[[package]] -name = "minidumper" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4ebc9d1f8847ec1d078f78b35ed598e0ebefa1f242d5f83cd8d7f03960a7d1" -dependencies = [ - "cfg-if", - "crash-context", - "libc", - "log", - "minidump-writer", - "parking_lot", - "polling", - "scroll", - "thiserror 1.0.69", - "uds", -] - [[package]] name = "minimal-lexical" version = "0.2.1" @@ -10576,15 +10431,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "nom" -version = "8.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" -dependencies = [ - "memchr", -] - [[package]] name = "noop_proc_macro" version = "0.3.0" @@ -11077,28 +10923,21 @@ dependencies = [ name = "onboarding" version = "0.1.0" dependencies = [ - "ai_onboarding", "anyhow", "client", + "command_palette_hooks", "component", "db", "documented", "editor", + "feature_flags", "fs", - "fuzzy", - "git", "gpui", - "itertools 0.14.0", "language", - "language_model", - "menu", - "notifications", - "picker", "project", "schemars", "serde", "settings", - "telemetry", "theme", "ui", "util", @@ -11174,7 +11013,6 @@ dependencies = [ "anyhow", "futures 0.3.31", "http_client", - "log", "schemars", "serde", "serde_json", @@ -11458,9 +11296,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -11468,9 +11306,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", @@ -11615,12 +11453,6 @@ dependencies = [ "hmac", ] -[[package]] -name = "pciid-parser" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0008e816fcdaf229cdd540e9b6ca2dc4a10d65c31624abb546c6420a02846e61" - [[package]] name = "pem" version = "3.0.5" @@ -12240,12 +12072,6 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" -[[package]] -name = "plain" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" - [[package]] name = "plist" version = "1.7.1" @@ -12506,16 +12332,6 @@ dependencies = [ "yansi", ] -[[package]] -name = "procfs-core" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d3554923a69f4ce04c4a754260c338f505ce22642d3830e049a399fc2059a29" -dependencies = [ - "bitflags 2.9.0", - "hex", -] - [[package]] name = "prodash" version = "29.0.2" @@ -12528,18 +12344,18 @@ dependencies = [ [[package]] name = "profiling" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" dependencies = [ "profiling-procmacros", ] [[package]] name = "profiling-procmacros" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" dependencies = [ "quote", "syn 2.0.101", @@ -12630,7 +12446,6 @@ dependencies = [ "editor", "file_icons", "git", - "git_ui", "gpui", "indexmap", "language", @@ -12644,7 +12459,6 @@ dependencies = [ "serde_json", "settings", "smallvec", - "telemetry", "theme", "ui", "util", @@ -13068,6 +12882,19 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + [[package]] name = "rand" version = "0.8.5" @@ -13089,6 +12916,16 @@ dependencies = [ "rand_core 0.9.3", ] +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -13109,6 +12946,15 @@ dependencies = [ "rand_core 0.9.3", ] +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + [[package]] name = "rand_core" version = "0.6.4" @@ -13128,12 +12974,12 @@ dependencies = [ ] [[package]] -name = "range-map" +name = "rand_hc" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12a5a2d6c7039059af621472a4389be1215a816df61aa4d531cfe85264aee95f" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" dependencies = [ - "num-traits", + "rand_core 0.5.1", ] [[package]] @@ -13468,7 +13314,6 @@ dependencies = [ name = "remote_server" version = "0.1.0" dependencies = [ - "action_log", "anyhow", "askpass", "assistant_tool", @@ -13479,8 +13324,6 @@ dependencies = [ "clap", "client", "clock", - "crash-handler", - "crashes", "dap", "dap_adapters", "debug_adapter_extension", @@ -13504,7 +13347,6 @@ dependencies = [ "libc", "log", "lsp", - "minidumper", "node_runtime", "paths", "project", @@ -13521,7 +13363,6 @@ dependencies = [ "smol", "sysinfo", "telemetry_events", - "thiserror 2.0.12", "toml 0.8.20", "unindent", "util", @@ -13694,7 +13535,6 @@ dependencies = [ "js-sys", "log", "mime", - "mime_guess", "once_cell", "percent-encoding", "pin-project-lite", @@ -13862,7 +13702,6 @@ checksum = "e40ecf59e742e03336be6a3d53755e789fd05a059fa22dfa0ed624722319e183" dependencies = [ "cpal", "dasp_sample", - "hound", "num-rational", "symphonia", "tracing", @@ -14361,10 +14200,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe8c9d1c68d67dd9f97ecbc6f932b60eb289c5dbddd8aa1405484a8fd2fcd984" dependencies = [ + "chrono", "dyn-clone", "indexmap", "ref-cast", "schemars_derive", + "semver", "serde", "serde_json", ] @@ -14422,26 +14263,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "scroll" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" -dependencies = [ - "scroll_derive", -] - -[[package]] -name = "scroll_derive" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", -] - [[package]] name = "scrypt" version = "0.11.0" @@ -14780,6 +14601,28 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_qs" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6" +dependencies = [ + "percent-encoding", + "serde", + "thiserror 1.0.69", +] + +[[package]] +name = "serde_qs" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cac3f1e2ca2fe333923a1ae72caca910b98ed0630bb35ef6f8c8517d6e81afa" +dependencies = [ + "percent-encoding", + "serde", + "thiserror 1.0.69", +] + [[package]] name = "serde_repr" version = "0.1.20" @@ -14921,10 +14764,8 @@ dependencies = [ "ui", "ui_input", "util", - "vim", "workspace", "workspace-hack", - "zed_actions", ] [[package]] @@ -15158,13 +14999,13 @@ dependencies = [ [[package]] name = "smart-default" -version = "0.7.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1" +checksum = "133659a15339456eeeb07572eb02a91c91e9815e9cbc89566944d2c8d3efdbf6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 1.0.109", ] [[package]] @@ -15184,6 +15025,15 @@ dependencies = [ "futures-lite 2.6.0", ] +[[package]] +name = "smol_str" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad6c857cbab2627dcf01ec85a623ca4e7dcb5691cbaa3d7fb7653671f0d09c9" +dependencies = [ + "serde", +] + [[package]] name = "smol_str" version = "0.2.2" @@ -15335,7 +15185,7 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" dependencies = [ - "nom 7.1.3", + "nom", "unicode_categories", ] @@ -15555,40 +15405,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" -[[package]] -name = "stacker" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cddb07e32ddb770749da91081d8d0ac3a16f1a569a18b20348cd371f5dead06b" -dependencies = [ - "cc", - "cfg-if", - "libc", - "psm", - "windows-sys 0.59.0", -] - -[[package]] -name = "stacksafe" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d9c1172965d317e87ddb6d364a040d958b40a1db82b6ef97da26253a8b3d090" -dependencies = [ - "stacker", - "stacksafe-macro", -] - -[[package]] -name = "stacksafe-macro" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "172175341049678163e979d9107ca3508046d4d2a7c6682bee46ac541b17db69" -dependencies = [ - "proc-macro-error2", - "quote", - "syn 2.0.101", -] - [[package]] name = "static_assertions" version = "1.1.0" @@ -15774,12 +15590,12 @@ dependencies = [ "anyhow", "client", "collections", - "edit_prediction", "editor", "env_logger 0.11.8", "futures 0.3.31", "gpui", "http_client", + "inline_completion", "language", "log", "postage", @@ -16140,21 +15956,6 @@ dependencies = [ "winx", ] -[[package]] -name = "system_specs" -version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "gpui", - "human_bytes", - "pciid-parser", - "release_channel", - "serde", - "sysinfo", - "workspace-hack", -] - [[package]] name = "tab_switcher" version = "0.1.0" @@ -16184,9 +15985,9 @@ dependencies = [ [[package]] name = "taffy" -version = "0.9.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13e5d13f79d558b5d353a98072ca8ca0e99da429467804de959aa8c83c9a004" +checksum = "7aaef0ac998e6527d6d0d5582f7e43953bb17221ac75bb8eb2fcc2db3396db1c" dependencies = [ "arrayvec", "grid", @@ -16387,7 +16188,7 @@ version = "0.1.0" dependencies = [ "anyhow", "assistant_slash_command", - "async-recursion", + "async-recursion 1.1.1", "breadcrumbs", "client", "collections", @@ -16448,7 +16249,7 @@ version = "0.1.0" dependencies = [ "anyhow", "collections", - "derive_more", + "derive_more 0.99.19", "fs", "futures 0.3.31", "gpui", @@ -16587,8 +16388,9 @@ dependencies = [ [[package]] name = "tiktoken-rs" -version = "0.8.0" -source = "git+https://github.com/zed-industries/tiktoken-rs?rev=30c32a4522751699adeda0d5840c71c3b75ae73d#30c32a4522751699adeda0d5840c71c3b75ae73d" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25563eeba904d770acf527e8b370fe9a5547bacd20ff84a0b6c3bc41288e5625" dependencies = [ "anyhow", "base64 0.22.1", @@ -17489,15 +17291,6 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" -[[package]] -name = "uds" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "885c31f06fce836457fe3ef09a59f83fe8db95d270b11cd78f40a4666c4d1661" -dependencies = [ - "libc", -] - [[package]] name = "uds_windows" version = "1.1.0" @@ -17934,7 +17727,6 @@ dependencies = [ "command_palette_hooks", "db", "editor", - "env_logger 0.11.8", "futures 0.3.31", "git_ui", "gpui", @@ -18081,6 +17873,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -18314,7 +18112,7 @@ dependencies = [ "indexmap", "libc", "log", - "mach2 0.4.2", + "mach2", "memfd", "object", "once_cell", @@ -18782,6 +18580,33 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" +[[package]] +name = "welcome" +version = "0.1.0" +dependencies = [ + "anyhow", + "client", + "component", + "db", + "documented", + "editor", + "fuzzy", + "gpui", + "install_cli", + "language", + "picker", + "project", + "serde", + "settings", + "telemetry", + "ui", + "util", + "vim_mode_setting", + "workspace", + "workspace-hack", + "zed_actions", +] + [[package]] name = "which" version = "4.4.2" @@ -19788,7 +19613,8 @@ version = "0.1.0" dependencies = [ "any_vec", "anyhow", - "async-recursion", + "async-recursion 1.1.1", + "bincode", "call", "client", "clock", @@ -19807,7 +19633,6 @@ dependencies = [ "node_runtime", "parking_lot", "postage", - "pretty_assertions", "project", "remote", "schemars", @@ -19921,13 +19746,11 @@ dependencies = [ "lyon_path", "md-5", "memchr", - "mime_guess", "miniz_oxide", "mio 1.0.3", "naga", - "nix 0.28.0", "nix 0.29.0", - "nom 7.1.3", + "nom", "num-bigint", "num-bigint-dig", "num-integer", @@ -19963,6 +19786,7 @@ dependencies = [ "rustix 1.0.7", "rustls 0.23.26", "rustls-webpki 0.103.1", + "schemars", "scopeguard", "sea-orm", "sea-query-binder", @@ -20166,7 +19990,7 @@ dependencies = [ [[package]] name = "xim" version = "0.4.0" -source = "git+https://github.com/zed-industries/xim-rs?rev=c0a70c1bd2ce197364216e5e818a2cb3adb99a8d#c0a70c1bd2ce197364216e5e818a2cb3adb99a8d" +source = "git+https://github.com/XDeme1/xim-rs?rev=d50d461764c2213655cd9cf65a0ea94c70d3c4fd#d50d461764c2213655cd9cf65a0ea94c70d3c4fd" dependencies = [ "ahash 0.8.11", "hashbrown 0.14.5", @@ -20179,7 +20003,7 @@ dependencies = [ [[package]] name = "xim-ctext" version = "0.3.0" -source = "git+https://github.com/zed-industries/xim-rs?rev=c0a70c1bd2ce197364216e5e818a2cb3adb99a8d#c0a70c1bd2ce197364216e5e818a2cb3adb99a8d" +source = "git+https://github.com/XDeme1/xim-rs?rev=d50d461764c2213655cd9cf65a0ea94c70d3c4fd#d50d461764c2213655cd9cf65a0ea94c70d3c4fd" dependencies = [ "encoding_rs", ] @@ -20187,7 +20011,7 @@ dependencies = [ [[package]] name = "xim-parser" version = "0.2.1" -source = "git+https://github.com/zed-industries/xim-rs?rev=c0a70c1bd2ce197364216e5e818a2cb3adb99a8d#c0a70c1bd2ce197364216e5e818a2cb3adb99a8d" +source = "git+https://github.com/XDeme1/xim-rs?rev=d50d461764c2213655cd9cf65a0ea94c70d3c4fd#d50d461764c2213655cd9cf65a0ea94c70d3c4fd" dependencies = [ "bitflags 2.9.0", ] @@ -20261,35 +20085,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" -[[package]] -name = "yawc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19a5d82922135b4ae73a079a4ffb5501e9aadb4d785b8c660eaa0a8b899028c5" -dependencies = [ - "base64 0.22.1", - "bytes 1.10.1", - "flate2", - "futures 0.3.31", - "http-body-util", - "hyper 1.6.0", - "hyper-util", - "js-sys", - "nom 8.0.0", - "pin-project", - "rand 0.8.5", - "sha1", - "thiserror 1.0.69", - "tokio", - "tokio-rustls 0.26.2", - "tokio-util", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "webpki-roots", -] - [[package]] name = "yazi" version = "0.2.1" @@ -20343,7 +20138,7 @@ dependencies = [ "async-io", "async-lock", "async-process", - "async-recursion", + "async-recursion 1.1.1", "async-task", "async-trait", "blocking", @@ -20396,9 +20191,8 @@ dependencies = [ [[package]] name = "zed" -version = "0.202.0" +version = "0.199.0" dependencies = [ - "acp_tools", "activity_indicator", "agent", "agent_servers", @@ -20414,7 +20208,6 @@ dependencies = [ "auto_update", "auto_update_ui", "backtrace", - "bincode", "breadcrumbs", "call", "channel", @@ -20427,7 +20220,6 @@ dependencies = [ "command_palette", "component", "copilot", - "crashes", "dap", "dap_adapters", "db", @@ -20435,7 +20227,6 @@ dependencies = [ "debugger_tools", "debugger_ui", "diagnostics", - "edit_prediction_button", "editor", "env_logger 0.11.8", "extension", @@ -20455,6 +20246,7 @@ dependencies = [ "http_client", "image_viewer", "indoc", + "inline_completion_button", "inspector_ui", "install_cli", "itertools 0.14.0", @@ -20468,7 +20260,6 @@ dependencies = [ "language_tools", "languages", "libc", - "livekit_client", "log", "markdown", "markdown_preview", @@ -20496,7 +20287,6 @@ dependencies = [ "release_channel", "remote", "repl", - "reqwest 0.12.15 (git+https://github.com/zed-industries/reqwest.git?rev=951c770a32f1998d6e999cef3e59e0013e6c4415)", "reqwest_client", "rope", "search", @@ -20513,7 +20303,6 @@ dependencies = [ "supermaven", "svg_preview", "sysinfo", - "system_specs", "tab_switcher", "task", "tasks_ui", @@ -20540,6 +20329,7 @@ dependencies = [ "watch", "web_search", "web_search_providers", + "welcome", "windows 0.61.1", "winresource", "workspace", @@ -20561,6 +20351,13 @@ dependencies = [ "workspace-hack", ] +[[package]] +name = "zed_emmet" +version = "0.0.4" +dependencies = [ + "zed_extension_api 0.1.0", +] + [[package]] name = "zed_extension_api" version = "0.1.0" @@ -20604,7 +20401,7 @@ dependencies = [ [[package]] name = "zed_ruff" -version = "0.1.1" +version = "0.1.0" dependencies = [ "zed_extension_api 0.1.0", ] @@ -20781,7 +20578,6 @@ dependencies = [ "copilot", "ctor", "db", - "edit_prediction", "editor", "feature_flags", "fs", @@ -20789,13 +20585,13 @@ dependencies = [ "gpui", "http_client", "indoc", + "inline_completion", "language", "language_model", "log", "menu", "postage", "project", - "rand 0.8.5", "regex", "release_channel", "reqwest_client", @@ -20820,42 +20616,6 @@ dependencies = [ "zlog", ] -[[package]] -name = "zeta_cli" -version = "0.1.0" -dependencies = [ - "anyhow", - "clap", - "client", - "debug_adapter_extension", - "extension", - "fs", - "futures 0.3.31", - "gpui", - "gpui_tokio", - "language", - "language_extension", - "language_model", - "language_models", - "languages", - "node_runtime", - "paths", - "project", - "prompt_store", - "release_channel", - "reqwest_client", - "serde", - "serde_json", - "settings", - "shellexpand 2.1.2", - "smol", - "terminal_view", - "util", - "watch", - "workspace-hack", - "zeta", -] - [[package]] name = "zip" version = "0.6.6" diff --git a/Cargo.toml b/Cargo.toml index 6ec243a9b9..0ad7a8e5ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,9 @@ [workspace] resolver = "2" members = [ - "crates/acp_tools", "crates/acp_thread", - "crates/action_log", "crates/activity_indicator", "crates/agent", - "crates/agent2", "crates/agent_servers", "crates/agent_settings", "crates/agent_ui", @@ -43,7 +40,6 @@ members = [ "crates/component", "crates/context_server", "crates/copilot", - "crates/crashes", "crates/credentials_provider", "crates/dap", "crates/dap_adapters", @@ -82,8 +78,9 @@ members = [ "crates/http_client_tls", "crates/icons", "crates/image_viewer", - "crates/edit_prediction", - "crates/edit_prediction_button", + "crates/indexed_docs", + "crates/inline_completion", + "crates/inline_completion_button", "crates/inspector_ui", "crates/install_cli", "crates/jj", @@ -156,7 +153,6 @@ members = [ "crates/streaming_diff", "crates/sum_tree", "crates/supermaven", - "crates/system_specs", "crates/supermaven_api", "crates/svg_preview", "crates/tab_switcher", @@ -186,13 +182,13 @@ members = [ "crates/watch", "crates/web_search", "crates/web_search_providers", + "crates/welcome", "crates/workspace", "crates/worktree", "crates/x_ai", "crates/zed", "crates/zed_actions", "crates/zeta", - "crates/zeta_cli", "crates/zlog", "crates/zlog_settings", @@ -200,6 +196,7 @@ members = [ # Extensions # + "extensions/emmet", "extensions/glsl", "extensions/html", "extensions/proto", @@ -228,11 +225,8 @@ 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" } -agent2 = { path = "crates/agent2" } activity_indicator = { path = "crates/activity_indicator" } agent_ui = { path = "crates/agent_ui" } agent_settings = { path = "crates/agent_settings" } @@ -271,7 +265,6 @@ command_palette_hooks = { path = "crates/command_palette_hooks" } component = { path = "crates/component" } context_server = { path = "crates/context_server" } copilot = { path = "crates/copilot" } -crashes = { path = "crates/crashes" } credentials_provider = { path = "crates/credentials_provider" } dap = { path = "crates/dap" } dap_adapters = { path = "crates/dap_adapters" } @@ -307,8 +300,9 @@ http_client = { path = "crates/http_client" } http_client_tls = { path = "crates/http_client_tls" } icons = { path = "crates/icons" } image_viewer = { path = "crates/image_viewer" } -edit_prediction = { path = "crates/edit_prediction" } -edit_prediction_button = { path = "crates/edit_prediction_button" } +indexed_docs = { path = "crates/indexed_docs" } +inline_completion = { path = "crates/inline_completion" } +inline_completion_button = { path = "crates/inline_completion_button" } inspector_ui = { path = "crates/inspector_ui" } install_cli = { path = "crates/install_cli" } jj = { path = "crates/jj" } @@ -363,7 +357,6 @@ remote_server = { path = "crates/remote_server" } repl = { path = "crates/repl" } reqwest_client = { path = "crates/reqwest_client" } rich_text = { path = "crates/rich_text" } -rodio = { version = "0.21.1", default-features = false } rope = { path = "crates/rope" } rpc = { path = "crates/rpc" } rules_library = { path = "crates/rules_library" } @@ -384,7 +377,6 @@ 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" } @@ -413,6 +405,7 @@ vim_mode_setting = { path = "crates/vim_mode_setting" } watch = { path = "crates/watch" } web_search = { path = "crates/web_search" } web_search_providers = { path = "crates/web_search_providers" } +welcome = { path = "crates/welcome" } workspace = { path = "crates/workspace" } worktree = { path = "crates/worktree" } x_ai = { path = "crates/x_ai" } @@ -426,7 +419,8 @@ zlog_settings = { path = "crates/zlog_settings" } # External crates # -agent-client-protocol = "0.0.31" +agentic-coding-protocol = "0.0.10" +agent-client-protocol = "0.0.11" aho-corasick = "1.1" alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" } any_vec = "0.14" @@ -453,7 +447,6 @@ 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"] } 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" } @@ -463,7 +456,6 @@ bytes = "1.0" cargo_metadata = "0.19" cargo_toml = "0.21" chrono = { version = "0.4", features = ["serde"] } -ciborium = "0.2" circular-buffer = "1.0" clap = { version = "4.4", features = ["derive"] } cocoa = "0.26" @@ -473,7 +465,6 @@ core-foundation = "0.10.0" core-foundation-sys = "0.8.6" core-video = { version = "0.4.3", features = ["metal"] } cpal = "0.16" -crash-handler = "0.6" criterion = { version = "0.5", features = ["html_reports"] } ctor = "0.4.0" dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "1b461b310481d01e02b2603c16d7144b926339f8" } @@ -497,7 +488,6 @@ 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" @@ -520,10 +510,8 @@ 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" } -mach2 = "0.5" markup5ever_rcdom = "0.3.0" metal = "0.29" -minidumper = "0.8" moka = { version = "0.12.10", features = ["sync"] } naga = { version = "25.0", features = ["wgsl-in"] } nanoid = "0.4" @@ -537,7 +525,6 @@ 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" } @@ -550,7 +537,7 @@ portable-pty = "0.9.0" postage = { version = "0.5", features = ["futures-traits"] } pretty_assertions = { version = "1.3.0", features = ["unstable"] } proc-macro2 = "1.0.93" -profiling = "1" +profiling = "1.0.17" prost = "0.9" prost-build = "0.9" prost-types = "0.9" @@ -564,7 +551,6 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "951c77 "charset", "http2", "macos-system-configuration", - "multipart", "rustls-tls-native-roots", "socks", "stream", @@ -589,7 +575,6 @@ serde_json_lenient = { version = "0.2", features = [ "raw_value", ] } serde_repr = "0.1" -serde_urlencoded = "0.7" sha2 = "0.10" shellexpand = "2.1.0" shlex = "1.3.0" @@ -597,7 +582,6 @@ 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"] } @@ -608,7 +592,7 @@ sysinfo = "0.31.0" take-until = "0.2.0" tempfile = "3.20.0" thiserror = "2.0.12" -tiktoken-rs = { git = "https://github.com/zed-industries/tiktoken-rs", rev = "30c32a4522751699adeda0d5840c71c3b75ae73d" } +tiktoken-rs = "0.7.0" time = { version = "0.3", features = [ "macros", "parsing", @@ -668,9 +652,22 @@ which = "6.0.0" windows-core = "0.61" wit-component = "0.221" workspace-hack = "0.1.0" -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 = [ @@ -681,6 +678,8 @@ features = [ "UI_ViewManagement", "Wdk_System_SystemServices", "Win32_Globalization", + "Win32_Graphics_Direct2D", + "Win32_Graphics_Direct2D_Common", "Win32_Graphics_Direct3D", "Win32_Graphics_Direct3D11", "Win32_Graphics_Direct3D_Fxc", @@ -691,6 +690,7 @@ features = [ "Win32_Graphics_Dxgi_Common", "Win32_Graphics_Gdi", "Win32_Graphics_Imaging", + "Win32_Graphics_Imaging_D2D", "Win32_Networking_WinSock", "Win32_Security", "Win32_Security_Credentials", @@ -703,7 +703,6 @@ features = [ "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Ole", - "Win32_System_Performance", "Win32_System_Pipes", "Win32_System_SystemInformation", "Win32_System_SystemServices", @@ -759,7 +758,7 @@ feature_flags = { codegen-units = 1 } file_icons = { codegen-units = 1 } fsevent = { codegen-units = 1 } image_viewer = { codegen-units = 1 } -edit_prediction_button = { codegen-units = 1 } +inline_completion_button = { codegen-units = 1 } install_cli = { codegen-units = 1 } journal = { codegen-units = 1 } lmstudio = { codegen-units = 1 } @@ -808,33 +807,38 @@ unexpected_cfgs = { level = "allow" } dbg_macro = "deny" todo = "deny" -# 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" - -# We currently do not restrict any style rules -# as it slows down shipping code to Zed. -# -# 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. -# -# 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 } - -# Individual rules that have violations in the codebase: -type_complexity = "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" +# These are all of the rules that currently have violations in the Zed +# codebase. +# +# 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. +# +# 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. +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" } +# 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" + # in Rust it can be very tedious to reduce argument count without # running afoul of the borrow checker. too_many_arguments = "allow" diff --git a/Dockerfile-collab b/Dockerfile-collab index c1621d6ee6..2dafe296c7 100644 --- a/Dockerfile-collab +++ b/Dockerfile-collab @@ -1,6 +1,6 @@ # syntax = docker/dockerfile:1.2 -FROM rust:1.89-bookworm as builder +FROM rust:1.88-bookworm as builder WORKDIR app COPY . . diff --git a/Procfile b/Procfile index b3f13f66a6..5f1231b90a 100644 --- a/Procfile +++ b/Procfile @@ -1,4 +1,3 @@ collab: RUST_LOG=${RUST_LOG:-info} cargo run --package=collab serve all -cloud: cd ../cloud; cargo make dev livekit: livekit-server --dev blob_store: ./script/run-local-minio diff --git a/Procfile.web b/Procfile.web deleted file mode 100644 index 8140555144..0000000000 --- a/Procfile.web +++ /dev/null @@ -1,2 +0,0 @@ -postgrest_llm: postgrest crates/collab/postgrest_llm.conf -website: cd ../zed.dev; npm run dev -- --port=3000 diff --git a/assets/fonts/ibm-plex-sans/IBMPlexSans-Bold.ttf b/assets/fonts/ibm-plex-sans/IBMPlexSans-Bold.ttf deleted file mode 100644 index 1d66b1a2e9..0000000000 Binary files a/assets/fonts/ibm-plex-sans/IBMPlexSans-Bold.ttf and /dev/null differ diff --git a/assets/fonts/ibm-plex-sans/IBMPlexSans-BoldItalic.ttf b/assets/fonts/ibm-plex-sans/IBMPlexSans-BoldItalic.ttf deleted file mode 100644 index e07bc1f527..0000000000 Binary files a/assets/fonts/ibm-plex-sans/IBMPlexSans-BoldItalic.ttf and /dev/null differ diff --git a/assets/fonts/ibm-plex-sans/IBMPlexSans-Italic.ttf b/assets/fonts/ibm-plex-sans/IBMPlexSans-Italic.ttf deleted file mode 100644 index efe8a1fb9d..0000000000 Binary files a/assets/fonts/ibm-plex-sans/IBMPlexSans-Italic.ttf and /dev/null differ diff --git a/assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf b/assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf deleted file mode 100644 index bd6817d520..0000000000 Binary files a/assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf and /dev/null differ diff --git a/assets/fonts/lilex/Lilex-Bold.ttf b/assets/fonts/lilex/Lilex-Bold.ttf deleted file mode 100644 index 45930ee30b..0000000000 Binary files a/assets/fonts/lilex/Lilex-Bold.ttf and /dev/null differ diff --git a/assets/fonts/lilex/Lilex-BoldItalic.ttf b/assets/fonts/lilex/Lilex-BoldItalic.ttf deleted file mode 100644 index 10c6ab5f74..0000000000 Binary files a/assets/fonts/lilex/Lilex-BoldItalic.ttf and /dev/null differ diff --git a/assets/fonts/lilex/Lilex-Italic.ttf b/assets/fonts/lilex/Lilex-Italic.ttf deleted file mode 100644 index e7aef10f7e..0000000000 Binary files a/assets/fonts/lilex/Lilex-Italic.ttf and /dev/null differ diff --git a/assets/fonts/lilex/Lilex-Regular.ttf b/assets/fonts/lilex/Lilex-Regular.ttf deleted file mode 100644 index cb98a69b0f..0000000000 Binary files a/assets/fonts/lilex/Lilex-Regular.ttf and /dev/null differ diff --git a/assets/fonts/plex-mono/ZedPlexMono-Bold.ttf b/assets/fonts/plex-mono/ZedPlexMono-Bold.ttf new file mode 100644 index 0000000000..d5f4b5e285 Binary files /dev/null and b/assets/fonts/plex-mono/ZedPlexMono-Bold.ttf differ diff --git a/assets/fonts/plex-mono/ZedPlexMono-BoldItalic.ttf b/assets/fonts/plex-mono/ZedPlexMono-BoldItalic.ttf new file mode 100644 index 0000000000..05eaf7cccd Binary files /dev/null and b/assets/fonts/plex-mono/ZedPlexMono-BoldItalic.ttf differ diff --git a/assets/fonts/plex-mono/ZedPlexMono-Italic.ttf b/assets/fonts/plex-mono/ZedPlexMono-Italic.ttf new file mode 100644 index 0000000000..3b07821757 Binary files /dev/null and b/assets/fonts/plex-mono/ZedPlexMono-Italic.ttf differ diff --git a/assets/fonts/plex-mono/ZedPlexMono-Regular.ttf b/assets/fonts/plex-mono/ZedPlexMono-Regular.ttf new file mode 100644 index 0000000000..61dbb58361 Binary files /dev/null and b/assets/fonts/plex-mono/ZedPlexMono-Regular.ttf differ diff --git a/assets/fonts/ibm-plex-sans/license.txt b/assets/fonts/plex-mono/license.txt similarity index 100% rename from assets/fonts/ibm-plex-sans/license.txt rename to assets/fonts/plex-mono/license.txt diff --git a/assets/fonts/plex-sans/ZedPlexSans-Bold.ttf b/assets/fonts/plex-sans/ZedPlexSans-Bold.ttf new file mode 100644 index 0000000000..f1e66392f7 Binary files /dev/null and b/assets/fonts/plex-sans/ZedPlexSans-Bold.ttf differ diff --git a/assets/fonts/plex-sans/ZedPlexSans-BoldItalic.ttf b/assets/fonts/plex-sans/ZedPlexSans-BoldItalic.ttf new file mode 100644 index 0000000000..7612dc5167 Binary files /dev/null and b/assets/fonts/plex-sans/ZedPlexSans-BoldItalic.ttf differ diff --git a/assets/fonts/plex-sans/ZedPlexSans-Italic.ttf b/assets/fonts/plex-sans/ZedPlexSans-Italic.ttf new file mode 100644 index 0000000000..8769c232ee Binary files /dev/null and b/assets/fonts/plex-sans/ZedPlexSans-Italic.ttf differ diff --git a/assets/fonts/plex-sans/ZedPlexSans-Regular.ttf b/assets/fonts/plex-sans/ZedPlexSans-Regular.ttf new file mode 100644 index 0000000000..3ea293d59a Binary files /dev/null and b/assets/fonts/plex-sans/ZedPlexSans-Regular.ttf differ diff --git a/assets/fonts/lilex/OFL.txt b/assets/fonts/plex-sans/license.txt similarity index 96% rename from assets/fonts/lilex/OFL.txt rename to assets/fonts/plex-sans/license.txt index 156240bc90..f72f76504c 100644 --- a/assets/fonts/lilex/OFL.txt +++ b/assets/fonts/plex-sans/license.txt @@ -1,9 +1,8 @@ -Copyright 2019 The Lilex Project Authors (https://github.com/mishamyrt/Lilex) +Copyright © 2017 IBM Corp. with Reserved Font Name "Plex" This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is copied below, and is also available with a FAQ at: -https://scripts.sil.org/OFL - +http://scripts.sil.org/OFL ----------------------------------------------------------- SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 @@ -90,4 +89,4 @@ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +OTHER DEALINGS IN THE FONT SOFTWARE. \ No newline at end of file diff --git a/assets/icons/ai.svg b/assets/icons/ai.svg index 4236d50337..d60396ad47 100644 --- a/assets/icons/ai.svg +++ b/assets/icons/ai.svg @@ -1,5 +1,5 @@ - + diff --git a/assets/icons/ai_bedrock.svg b/assets/icons/ai_bedrock.svg index c9bbcc82e1..2b672c364e 100644 --- a/assets/icons/ai_bedrock.svg +++ b/assets/icons/ai_bedrock.svg @@ -1,8 +1,4 @@ - - - - - - - + + + diff --git a/assets/icons/ai_deep_seek.svg b/assets/icons/ai_deep_seek.svg index c8e5483fb3..cf480c834c 100644 --- a/assets/icons/ai_deep_seek.svg +++ b/assets/icons/ai_deep_seek.svg @@ -1,3 +1 @@ - - - +DeepSeek diff --git a/assets/icons/ai_lm_studio.svg b/assets/icons/ai_lm_studio.svg index 5cfdeb5578..0b455f48a7 100644 --- a/assets/icons/ai_lm_studio.svg +++ b/assets/icons/ai_lm_studio.svg @@ -1,15 +1,33 @@ - - - - - - - - - - - - - - + + + Artboard + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/ai_mistral.svg b/assets/icons/ai_mistral.svg index f11c177e2f..23b8f2ef6c 100644 --- a/assets/icons/ai_mistral.svg +++ b/assets/icons/ai_mistral.svg @@ -1,8 +1 @@ - - - - - - - - +Mistral \ No newline at end of file diff --git a/assets/icons/ai_ollama.svg b/assets/icons/ai_ollama.svg index 36a88c1ad6..d433df3981 100644 --- a/assets/icons/ai_ollama.svg +++ b/assets/icons/ai_ollama.svg @@ -1,7 +1,14 @@ - - - - - + + + + + + + + + + + + diff --git a/assets/icons/ai_open_ai.svg b/assets/icons/ai_open_ai.svg index e45ac315a0..e659a472d8 100644 --- a/assets/icons/ai_open_ai.svg +++ b/assets/icons/ai_open_ai.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/ai_open_router.svg b/assets/icons/ai_open_router.svg index b6f5164e0b..94f2849146 100644 --- a/assets/icons/ai_open_router.svg +++ b/assets/icons/ai_open_router.svg @@ -1,8 +1,8 @@ - - - - - - - + + + + + + + diff --git a/assets/icons/ai_x_ai.svg b/assets/icons/ai_x_ai.svg index d3400fbe9c..289525c8ef 100644 --- a/assets/icons/ai_x_ai.svg +++ b/assets/icons/ai_x_ai.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/ai_zed.svg b/assets/icons/ai_zed.svg index 6d78efacd5..1c6bb8ad63 100644 --- a/assets/icons/ai_zed.svg +++ b/assets/icons/ai_zed.svg @@ -1,3 +1,10 @@ - + + + + + + + + diff --git a/assets/icons/arrow_circle.svg b/assets/icons/arrow_circle.svg index cdfa939795..90e352bdea 100644 --- a/assets/icons/arrow_circle.svg +++ b/assets/icons/arrow_circle.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/assets/icons/arrow_down.svg b/assets/icons/arrow_down.svg index 60e6584c45..7d78497e6d 100644 --- a/assets/icons/arrow_down.svg +++ b/assets/icons/arrow_down.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/arrow_down10.svg b/assets/icons/arrow_down10.svg index 5933b758d9..97ce967a8b 100644 --- a/assets/icons/arrow_down10.svg +++ b/assets/icons/arrow_down10.svg @@ -1 +1 @@ - + diff --git a/assets/icons/arrow_down_from_line.svg b/assets/icons/arrow_down_from_line.svg new file mode 100644 index 0000000000..89316973a0 --- /dev/null +++ b/assets/icons/arrow_down_from_line.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/arrow_down_right.svg b/assets/icons/arrow_down_right.svg index ebdb06d77b..b9c10263d0 100644 --- a/assets/icons/arrow_down_right.svg +++ b/assets/icons/arrow_down_right.svg @@ -1,4 +1 @@ - - - - + diff --git a/assets/icons/arrow_left.svg b/assets/icons/arrow_left.svg index f7eacb2a77..57ee750490 100644 --- a/assets/icons/arrow_left.svg +++ b/assets/icons/arrow_left.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/arrow_right.svg b/assets/icons/arrow_right.svg index b9324af5a2..7a5b1174eb 100644 --- a/assets/icons/arrow_right.svg +++ b/assets/icons/arrow_right.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/arrow_right_left.svg b/assets/icons/arrow_right_left.svg index 2c1211056a..30331960c9 100644 --- a/assets/icons/arrow_right_left.svg +++ b/assets/icons/arrow_right_left.svg @@ -1,6 +1 @@ - - - - - - + diff --git a/assets/icons/arrow_up.svg b/assets/icons/arrow_up.svg index ff3ad44123..81dfee8042 100644 --- a/assets/icons/arrow_up.svg +++ b/assets/icons/arrow_up.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/arrow_up_alt.svg b/assets/icons/arrow_up_alt.svg new file mode 100644 index 0000000000..c8cf286a8c --- /dev/null +++ b/assets/icons/arrow_up_alt.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/arrow_up_from_line.svg b/assets/icons/arrow_up_from_line.svg new file mode 100644 index 0000000000..50a075e42b --- /dev/null +++ b/assets/icons/arrow_up_from_line.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/arrow_up_right.svg b/assets/icons/arrow_up_right.svg index a948ef8f81..9fbafba4ec 100644 --- a/assets/icons/arrow_up_right.svg +++ b/assets/icons/arrow_up_right.svg @@ -1,4 +1 @@ - - - - + diff --git a/assets/icons/arrow_up_right_alt.svg b/assets/icons/arrow_up_right_alt.svg new file mode 100644 index 0000000000..4e923c6867 --- /dev/null +++ b/assets/icons/arrow_up_right_alt.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/at_sign.svg b/assets/icons/at_sign.svg new file mode 100644 index 0000000000..4cf8cd468f --- /dev/null +++ b/assets/icons/at_sign.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/attach.svg b/assets/icons/attach.svg deleted file mode 100644 index f923a3c7c8..0000000000 --- a/assets/icons/attach.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/audio_off.svg b/assets/icons/audio_off.svg index 43d2a04344..dfb5a1c458 100644 --- a/assets/icons/audio_off.svg +++ b/assets/icons/audio_off.svg @@ -1,7 +1,7 @@ - - - - - + + + + + diff --git a/assets/icons/audio_on.svg b/assets/icons/audio_on.svg index 6e183bd585..d1bef0d337 100644 --- a/assets/icons/audio_on.svg +++ b/assets/icons/audio_on.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/backspace.svg b/assets/icons/backspace.svg index 9ef4432b6f..f7f1cf107a 100644 --- a/assets/icons/backspace.svg +++ b/assets/icons/backspace.svg @@ -1,5 +1,3 @@ - - - - + + diff --git a/assets/icons/bell.svg b/assets/icons/bell.svg index 70225bb105..f9b2a97fb3 100644 --- a/assets/icons/bell.svg +++ b/assets/icons/bell.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/bell_dot.svg b/assets/icons/bell_dot.svg index 959a7773cf..09a17401da 100644 --- a/assets/icons/bell_dot.svg +++ b/assets/icons/bell_dot.svg @@ -1,5 +1,5 @@ - - + + diff --git a/assets/icons/bell_off.svg b/assets/icons/bell_off.svg index 5c3c1a0d68..98cbd1eb60 100644 --- a/assets/icons/bell_off.svg +++ b/assets/icons/bell_off.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/bell_ring.svg b/assets/icons/bell_ring.svg index 838056cc03..e411e7511b 100644 --- a/assets/icons/bell_ring.svg +++ b/assets/icons/bell_ring.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/assets/icons/binary.svg b/assets/icons/binary.svg index 3c15e9b547..8f5e456d16 100644 --- a/assets/icons/binary.svg +++ b/assets/icons/binary.svg @@ -1 +1 @@ - + diff --git a/assets/icons/blocks.svg b/assets/icons/blocks.svg index 84725d7892..588d49abbc 100644 --- a/assets/icons/blocks.svg +++ b/assets/icons/blocks.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/assets/icons/bolt.svg b/assets/icons/bolt.svg new file mode 100644 index 0000000000..2688ede2a5 --- /dev/null +++ b/assets/icons/bolt.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/bolt_filled.svg b/assets/icons/bolt_filled.svg index 14d8f53e02..543e72adf8 100644 --- a/assets/icons/bolt_filled.svg +++ b/assets/icons/bolt_filled.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/bolt_filled_alt.svg b/assets/icons/bolt_filled_alt.svg new file mode 100644 index 0000000000..141e1c5f57 --- /dev/null +++ b/assets/icons/bolt_filled_alt.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/bolt_outlined.svg b/assets/icons/bolt_outlined.svg deleted file mode 100644 index ca9c75fbfd..0000000000 --- a/assets/icons/bolt_outlined.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/book.svg b/assets/icons/book.svg index a2ab394be4..d30f81f32e 100644 --- a/assets/icons/book.svg +++ b/assets/icons/book.svg @@ -1 +1 @@ - + diff --git a/assets/icons/book_copy.svg b/assets/icons/book_copy.svg index b7afd1df5c..b055d47b5f 100644 --- a/assets/icons/book_copy.svg +++ b/assets/icons/book_copy.svg @@ -1 +1 @@ - + diff --git a/assets/icons/book_plus.svg b/assets/icons/book_plus.svg new file mode 100644 index 0000000000..2868f07cd0 --- /dev/null +++ b/assets/icons/book_plus.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/brain.svg b/assets/icons/brain.svg new file mode 100644 index 0000000000..80c93814f7 --- /dev/null +++ b/assets/icons/brain.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/bug_off.svg b/assets/icons/bug_off.svg new file mode 100644 index 0000000000..23f4ef06df --- /dev/null +++ b/assets/icons/bug_off.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/caret_down.svg b/assets/icons/caret_down.svg new file mode 100644 index 0000000000..ff8b8c3b88 --- /dev/null +++ b/assets/icons/caret_down.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/caret_up.svg b/assets/icons/caret_up.svg new file mode 100644 index 0000000000..53026b83d8 --- /dev/null +++ b/assets/icons/caret_up.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/case_sensitive.svg b/assets/icons/case_sensitive.svg index 015e241416..8c943e7509 100644 --- a/assets/icons/case_sensitive.svg +++ b/assets/icons/case_sensitive.svg @@ -1 +1,8 @@ - + + + + + + + + diff --git a/assets/icons/chat.svg b/assets/icons/chat.svg deleted file mode 100644 index c64f6b5e0e..0000000000 --- a/assets/icons/chat.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/check.svg b/assets/icons/check.svg index 21e2137965..39352682c9 100644 --- a/assets/icons/check.svg +++ b/assets/icons/check.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/check_circle.svg b/assets/icons/check_circle.svg index f9b88c4ce1..b48fe34631 100644 --- a/assets/icons/check_circle.svg +++ b/assets/icons/check_circle.svg @@ -1,4 +1,4 @@ - - - + + + diff --git a/assets/icons/check_double.svg b/assets/icons/check_double.svg index fabc700520..5c17d95a6b 100644 --- a/assets/icons/check_double.svg +++ b/assets/icons/check_double.svg @@ -1,4 +1 @@ - - - - + diff --git a/assets/icons/chevron_down.svg b/assets/icons/chevron_down.svg index e4ca142a91..b971555cfa 100644 --- a/assets/icons/chevron_down.svg +++ b/assets/icons/chevron_down.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/chevron_down_small.svg b/assets/icons/chevron_down_small.svg new file mode 100644 index 0000000000..8f8a99d4b9 --- /dev/null +++ b/assets/icons/chevron_down_small.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/chevron_left.svg b/assets/icons/chevron_left.svg index fbe438fd4b..8e61beed5d 100644 --- a/assets/icons/chevron_left.svg +++ b/assets/icons/chevron_left.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/chevron_right.svg b/assets/icons/chevron_right.svg index 4f170717c9..fcd9d83fc2 100644 --- a/assets/icons/chevron_right.svg +++ b/assets/icons/chevron_right.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/chevron_up.svg b/assets/icons/chevron_up.svg index bbe6b9762d..171cdd61c0 100644 --- a/assets/icons/chevron_up.svg +++ b/assets/icons/chevron_up.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/chevron_up_down.svg b/assets/icons/chevron_up_down.svg index 299f6bce5a..a7414ec8a0 100644 --- a/assets/icons/chevron_up_down.svg +++ b/assets/icons/chevron_up_down.svg @@ -1,4 +1 @@ - - - - + diff --git a/assets/icons/circle.svg b/assets/icons/circle.svg index 1d80edac09..67306cb12a 100644 --- a/assets/icons/circle.svg +++ b/assets/icons/circle.svg @@ -1,3 +1 @@ - - - + diff --git a/assets/icons/circle_check.svg b/assets/icons/circle_check.svg index 8950aa7a0e..adfc8cecca 100644 --- a/assets/icons/circle_check.svg +++ b/assets/icons/circle_check.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/circle_help.svg b/assets/icons/circle_help.svg index 0e623bd1da..1a004bfff8 100644 --- a/assets/icons/circle_help.svg +++ b/assets/icons/circle_help.svg @@ -1,5 +1 @@ - - - - - + diff --git a/assets/icons/circle_off.svg b/assets/icons/circle_off.svg new file mode 100644 index 0000000000..be1bf29225 --- /dev/null +++ b/assets/icons/circle_off.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/close.svg b/assets/icons/close.svg index 846b3a703d..31c5aa31a6 100644 --- a/assets/icons/close.svg +++ b/assets/icons/close.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/cloud.svg b/assets/icons/cloud.svg new file mode 100644 index 0000000000..73a9618067 --- /dev/null +++ b/assets/icons/cloud.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/cloud_download.svg b/assets/icons/cloud_download.svg index 70cda55856..bc7a8376d1 100644 --- a/assets/icons/cloud_download.svg +++ b/assets/icons/cloud_download.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/assets/icons/code.svg b/assets/icons/code.svg index 72d145224a..757c5a1cb6 100644 --- a/assets/icons/code.svg +++ b/assets/icons/code.svg @@ -1 +1 @@ - + diff --git a/assets/icons/cog.svg b/assets/icons/cog.svg index 7dd3a8beff..03c0a290b7 100644 --- a/assets/icons/cog.svg +++ b/assets/icons/cog.svg @@ -1 +1 @@ - + diff --git a/assets/icons/command.svg b/assets/icons/command.svg index f361ca2d05..d38389aea4 100644 --- a/assets/icons/command.svg +++ b/assets/icons/command.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/context.svg b/assets/icons/context.svg new file mode 100644 index 0000000000..837b3aadd9 --- /dev/null +++ b/assets/icons/context.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/control.svg b/assets/icons/control.svg index f9341b6256..94189dc07d 100644 --- a/assets/icons/control.svg +++ b/assets/icons/control.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/copilot.svg b/assets/icons/copilot.svg index 2584cd6310..06dbf178ae 100644 --- a/assets/icons/copilot.svg +++ b/assets/icons/copilot.svg @@ -1,9 +1,9 @@ - - - - - - - - + + + + + + + + diff --git a/assets/icons/copilot_disabled.svg b/assets/icons/copilot_disabled.svg index 90afa84966..eba36a2b69 100644 --- a/assets/icons/copilot_disabled.svg +++ b/assets/icons/copilot_disabled.svg @@ -1,9 +1,9 @@ - - - - + + + + - + diff --git a/assets/icons/copilot_error.svg b/assets/icons/copilot_error.svg index 77744e7529..6069c554f1 100644 --- a/assets/icons/copilot_error.svg +++ b/assets/icons/copilot_error.svg @@ -1,7 +1,7 @@ - - + + - + diff --git a/assets/icons/copilot_init.svg b/assets/icons/copilot_init.svg index 754d159584..6cbf63fb49 100644 --- a/assets/icons/copilot_init.svg +++ b/assets/icons/copilot_init.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/copy.svg b/assets/icons/copy.svg index aba193930b..7a3cdcf6da 100644 --- a/assets/icons/copy.svg +++ b/assets/icons/copy.svg @@ -1,4 +1,4 @@ - - - + + + diff --git a/assets/icons/countdown_timer.svg b/assets/icons/countdown_timer.svg index 5d1e775e68..b9b7479228 100644 --- a/assets/icons/countdown_timer.svg +++ b/assets/icons/countdown_timer.svg @@ -1 +1 @@ - + diff --git a/assets/icons/crosshair.svg b/assets/icons/crosshair.svg index 3af6aa9fa3..006c6362aa 100644 --- a/assets/icons/crosshair.svg +++ b/assets/icons/crosshair.svg @@ -1,7 +1,7 @@ - - - - - - + + + + + + diff --git a/assets/icons/cursor_i_beam.svg b/assets/icons/cursor_i_beam.svg index 2d513181f9..3790de6f49 100644 --- a/assets/icons/cursor_i_beam.svg +++ b/assets/icons/cursor_i_beam.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/dash.svg b/assets/icons/dash.svg index 3928ee7cfa..efff9eab5e 100644 --- a/assets/icons/dash.svg +++ b/assets/icons/dash.svg @@ -1 +1 @@ - + diff --git a/assets/icons/database_zap.svg b/assets/icons/database_zap.svg index 76af0f9251..06241b35f4 100644 --- a/assets/icons/database_zap.svg +++ b/assets/icons/database_zap.svg @@ -1 +1 @@ - + diff --git a/assets/icons/debug.svg b/assets/icons/debug.svg index 6423a2b090..ff51e42b1a 100644 --- a/assets/icons/debug.svg +++ b/assets/icons/debug.svg @@ -1,12 +1,12 @@ - - - - - - - - - - + + + + + + + + + + diff --git a/assets/icons/debug_breakpoint.svg b/assets/icons/debug_breakpoint.svg index c09a3c159f..f6a7b35658 100644 --- a/assets/icons/debug_breakpoint.svg +++ b/assets/icons/debug_breakpoint.svg @@ -1,3 +1 @@ - - - + diff --git a/assets/icons/debug_continue.svg b/assets/icons/debug_continue.svg index f03a8b2364..e2a99c38d0 100644 --- a/assets/icons/debug_continue.svg +++ b/assets/icons/debug_continue.svg @@ -1 +1 @@ - + diff --git a/assets/icons/debug_detach.svg b/assets/icons/debug_detach.svg index 8b34845571..0eb2537152 100644 --- a/assets/icons/debug_detach.svg +++ b/assets/icons/debug_detach.svg @@ -1 +1 @@ - + diff --git a/assets/icons/debug_disabled_breakpoint.svg b/assets/icons/debug_disabled_breakpoint.svg index 9a7c896f47..a7260ec04b 100644 --- a/assets/icons/debug_disabled_breakpoint.svg +++ b/assets/icons/debug_disabled_breakpoint.svg @@ -1,3 +1 @@ - - - + diff --git a/assets/icons/debug_disabled_log_breakpoint.svg b/assets/icons/debug_disabled_log_breakpoint.svg index f477f4f32d..d0bb2c8e2b 100644 --- a/assets/icons/debug_disabled_log_breakpoint.svg +++ b/assets/icons/debug_disabled_log_breakpoint.svg @@ -1,5 +1 @@ - - - - - + diff --git a/assets/icons/debug_ignore_breakpoints.svg b/assets/icons/debug_ignore_breakpoints.svg index bc95329c7a..ba7074e083 100644 --- a/assets/icons/debug_ignore_breakpoints.svg +++ b/assets/icons/debug_ignore_breakpoints.svg @@ -1,3 +1 @@ - - - + diff --git a/assets/icons/debug_log_breakpoint.svg b/assets/icons/debug_log_breakpoint.svg index 22eae9d029..a878ce3e04 100644 --- a/assets/icons/debug_log_breakpoint.svg +++ b/assets/icons/debug_log_breakpoint.svg @@ -1,3 +1 @@ - - - + diff --git a/assets/icons/debug_pause.svg b/assets/icons/debug_pause.svg index 65e1949581..bea531bc5a 100644 --- a/assets/icons/debug_pause.svg +++ b/assets/icons/debug_pause.svg @@ -1,4 +1 @@ - - - - + diff --git a/assets/icons/debug_restart.svg b/assets/icons/debug_restart.svg new file mode 100644 index 0000000000..4eff13b94b --- /dev/null +++ b/assets/icons/debug_restart.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/debug_step_back.svg b/assets/icons/debug_step_back.svg index 61d45866f6..bc7c9b8444 100644 --- a/assets/icons/debug_step_back.svg +++ b/assets/icons/debug_step_back.svg @@ -1 +1 @@ - + diff --git a/assets/icons/debug_step_into.svg b/assets/icons/debug_step_into.svg index 9a517fc7ca..69e5cff3f1 100644 --- a/assets/icons/debug_step_into.svg +++ b/assets/icons/debug_step_into.svg @@ -1 +1,5 @@ - + + + + + diff --git a/assets/icons/debug_step_out.svg b/assets/icons/debug_step_out.svg index 147a44f930..680e13e65e 100644 --- a/assets/icons/debug_step_out.svg +++ b/assets/icons/debug_step_out.svg @@ -1 +1,5 @@ - + + + + + diff --git a/assets/icons/debug_step_over.svg b/assets/icons/debug_step_over.svg index 336abc11de..005b901da3 100644 --- a/assets/icons/debug_step_over.svg +++ b/assets/icons/debug_step_over.svg @@ -1 +1,5 @@ - + + + + + diff --git a/assets/icons/debug_stop.svg b/assets/icons/debug_stop.svg new file mode 100644 index 0000000000..fef651c586 --- /dev/null +++ b/assets/icons/debug_stop.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/delete.svg b/assets/icons/delete.svg new file mode 100644 index 0000000000..a7edbb6158 --- /dev/null +++ b/assets/icons/delete.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/diff.svg b/assets/icons/diff.svg index 9d93b2d5b4..ca43c379da 100644 --- a/assets/icons/diff.svg +++ b/assets/icons/diff.svg @@ -1 +1 @@ - + diff --git a/assets/icons/disconnected.svg b/assets/icons/disconnected.svg index 47bd1db478..37d0ee904c 100644 --- a/assets/icons/disconnected.svg +++ b/assets/icons/disconnected.svg @@ -1 +1,3 @@ - + + + diff --git a/assets/icons/document_text.svg b/assets/icons/document_text.svg new file mode 100644 index 0000000000..78c08d92f9 --- /dev/null +++ b/assets/icons/document_text.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/download.svg b/assets/icons/download.svg index 6c105d3fd7..2ffa65e8ac 100644 --- a/assets/icons/download.svg +++ b/assets/icons/download.svg @@ -1 +1 @@ - + diff --git a/assets/icons/editor_atom.svg b/assets/icons/editor_atom.svg deleted file mode 100644 index cc5fa83843..0000000000 --- a/assets/icons/editor_atom.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/editor_cursor.svg b/assets/icons/editor_cursor.svg deleted file mode 100644 index 338697be8a..0000000000 --- a/assets/icons/editor_cursor.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/assets/icons/editor_emacs.svg b/assets/icons/editor_emacs.svg deleted file mode 100644 index 951d7b2be1..0000000000 --- a/assets/icons/editor_emacs.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/assets/icons/editor_jet_brains.svg b/assets/icons/editor_jet_brains.svg deleted file mode 100644 index 7d9cf0c65c..0000000000 --- a/assets/icons/editor_jet_brains.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/editor_sublime.svg b/assets/icons/editor_sublime.svg deleted file mode 100644 index 95a04f6b54..0000000000 --- a/assets/icons/editor_sublime.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/editor_vs_code.svg b/assets/icons/editor_vs_code.svg deleted file mode 100644 index 2a71ad52af..0000000000 --- a/assets/icons/editor_vs_code.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/ellipsis.svg b/assets/icons/ellipsis.svg index 22b5a8fd46..1858c65520 100644 --- a/assets/icons/ellipsis.svg +++ b/assets/icons/ellipsis.svg @@ -1,5 +1,5 @@ - - - - + + + + diff --git a/assets/icons/ellipsis_vertical.svg b/assets/icons/ellipsis_vertical.svg index c38437667e..077dbe8778 100644 --- a/assets/icons/ellipsis_vertical.svg +++ b/assets/icons/ellipsis_vertical.svg @@ -1,5 +1 @@ - - - - - + diff --git a/assets/icons/envelope.svg b/assets/icons/envelope.svg index 273cc6de26..0f5e95f968 100644 --- a/assets/icons/envelope.svg +++ b/assets/icons/envelope.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/equal.svg b/assets/icons/equal.svg new file mode 100644 index 0000000000..9b3a151a12 --- /dev/null +++ b/assets/icons/equal.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/eraser.svg b/assets/icons/eraser.svg index ca6209785f..edb893a8c6 100644 --- a/assets/icons/eraser.svg +++ b/assets/icons/eraser.svg @@ -1 +1,4 @@ - + + + + diff --git a/assets/icons/escape.svg b/assets/icons/escape.svg index 1898588a67..00c772a2ad 100644 --- a/assets/icons/escape.svg +++ b/assets/icons/escape.svg @@ -1 +1 @@ - + diff --git a/assets/icons/exit.svg b/assets/icons/exit.svg index 3619a55c87..1ff9d78824 100644 --- a/assets/icons/exit.svg +++ b/assets/icons/exit.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/expand_down.svg b/assets/icons/expand_down.svg index 9f85ee6720..a17b9e285c 100644 --- a/assets/icons/expand_down.svg +++ b/assets/icons/expand_down.svg @@ -1,4 +1,4 @@ - - - + + + diff --git a/assets/icons/expand_up.svg b/assets/icons/expand_up.svg index 49b084fa8f..30f9af92e3 100644 --- a/assets/icons/expand_up.svg +++ b/assets/icons/expand_up.svg @@ -1,4 +1,4 @@ - - - + + + diff --git a/assets/icons/expand_vertical.svg b/assets/icons/expand_vertical.svg index 5a5fa8ccb5..e278911478 100644 --- a/assets/icons/expand_vertical.svg +++ b/assets/icons/expand_vertical.svg @@ -1 +1 @@ - + diff --git a/assets/icons/external_link.svg b/assets/icons/external_link.svg new file mode 100644 index 0000000000..561f012452 --- /dev/null +++ b/assets/icons/external_link.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/eye.svg b/assets/icons/eye.svg index 327fa751e9..21e3d3ba63 100644 --- a/assets/icons/eye.svg +++ b/assets/icons/eye.svg @@ -1,4 +1 @@ - - - - + diff --git a/assets/icons/file.svg b/assets/icons/file.svg index 60cf2537d9..5b1b892756 100644 --- a/assets/icons/file.svg +++ b/assets/icons/file.svg @@ -1 +1,4 @@ - + + + + diff --git a/assets/icons/file_code.svg b/assets/icons/file_code.svg index 548d5a153b..0a15da7705 100644 --- a/assets/icons/file_code.svg +++ b/assets/icons/file_code.svg @@ -1 +1 @@ - + diff --git a/assets/icons/file_create.svg b/assets/icons/file_create.svg new file mode 100644 index 0000000000..bd7f88a7ec --- /dev/null +++ b/assets/icons/file_create.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/file_diff.svg b/assets/icons/file_diff.svg index 193dd7392f..ff20f16c60 100644 --- a/assets/icons/file_diff.svg +++ b/assets/icons/file_diff.svg @@ -1 +1 @@ - + diff --git a/assets/icons/file_doc.svg b/assets/icons/file_doc.svg index ccd5eeea01..3b11995f36 100644 --- a/assets/icons/file_doc.svg +++ b/assets/icons/file_doc.svg @@ -1,6 +1,6 @@ - + - - + + diff --git a/assets/icons/file_generic.svg b/assets/icons/file_generic.svg index 790a5f18d7..3c72bd3320 100644 --- a/assets/icons/file_generic.svg +++ b/assets/icons/file_generic.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/file_git.svg b/assets/icons/file_git.svg index 2b36b0ffd3..197db2e9e6 100644 --- a/assets/icons/file_git.svg +++ b/assets/icons/file_git.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/assets/icons/file_icons/ai.svg b/assets/icons/file_icons/ai.svg index 4236d50337..d60396ad47 100644 --- a/assets/icons/file_icons/ai.svg +++ b/assets/icons/file_icons/ai.svg @@ -1,5 +1,5 @@ - + diff --git a/assets/icons/file_icons/audio.svg b/assets/icons/file_icons/audio.svg index 7948b04616..672f736c95 100644 --- a/assets/icons/file_icons/audio.svg +++ b/assets/icons/file_icons/audio.svg @@ -1,8 +1,8 @@ - - - - - - + + + + + + diff --git a/assets/icons/file_icons/book.svg b/assets/icons/file_icons/book.svg index ccd5eeea01..3b11995f36 100644 --- a/assets/icons/file_icons/book.svg +++ b/assets/icons/file_icons/book.svg @@ -1,6 +1,6 @@ - + - - + + diff --git a/assets/icons/file_icons/bun.svg b/assets/icons/file_icons/bun.svg index ca1ec900bc..48af8b3088 100644 --- a/assets/icons/file_icons/bun.svg +++ b/assets/icons/file_icons/bun.svg @@ -1,5 +1,5 @@ - + diff --git a/assets/icons/file_icons/chevron_down.svg b/assets/icons/file_icons/chevron_down.svg index 9918f6c9f7..9e60e40cf4 100644 --- a/assets/icons/file_icons/chevron_down.svg +++ b/assets/icons/file_icons/chevron_down.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/file_icons/chevron_left.svg b/assets/icons/file_icons/chevron_left.svg index 3299ee7168..a2aa9ad996 100644 --- a/assets/icons/file_icons/chevron_left.svg +++ b/assets/icons/file_icons/chevron_left.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/file_icons/chevron_right.svg b/assets/icons/file_icons/chevron_right.svg index 140f644127..06608c95ee 100644 --- a/assets/icons/file_icons/chevron_right.svg +++ b/assets/icons/file_icons/chevron_right.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/file_icons/chevron_up.svg b/assets/icons/file_icons/chevron_up.svg index ae8c12a989..fd3d5e4470 100644 --- a/assets/icons/file_icons/chevron_up.svg +++ b/assets/icons/file_icons/chevron_up.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/file_icons/code.svg b/assets/icons/file_icons/code.svg index af2f6c5dc0..5f012f8838 100644 --- a/assets/icons/file_icons/code.svg +++ b/assets/icons/file_icons/code.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/file_icons/coffeescript.svg b/assets/icons/file_icons/coffeescript.svg index e91d187615..fc49df62c0 100644 --- a/assets/icons/file_icons/coffeescript.svg +++ b/assets/icons/file_icons/coffeescript.svg @@ -1,5 +1,5 @@ - + diff --git a/assets/icons/file_icons/conversations.svg b/assets/icons/file_icons/conversations.svg index e25ed973ef..cef764661f 100644 --- a/assets/icons/file_icons/conversations.svg +++ b/assets/icons/file_icons/conversations.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/file_icons/dart.svg b/assets/icons/file_icons/dart.svg index c9ec3de51a..fd3ab01c93 100644 --- a/assets/icons/file_icons/dart.svg +++ b/assets/icons/file_icons/dart.svg @@ -1,5 +1,5 @@ - + diff --git a/assets/icons/file_icons/database.svg b/assets/icons/file_icons/database.svg index a8226110d3..10fbdcbff4 100644 --- a/assets/icons/file_icons/database.svg +++ b/assets/icons/file_icons/database.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/file_icons/diff.svg b/assets/icons/file_icons/diff.svg index ec59a0aabe..07c46f1799 100644 --- a/assets/icons/file_icons/diff.svg +++ b/assets/icons/file_icons/diff.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/file_icons/eslint.svg b/assets/icons/file_icons/eslint.svg index ba72d9166b..0f42abe691 100644 --- a/assets/icons/file_icons/eslint.svg +++ b/assets/icons/file_icons/eslint.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/file_icons/file.svg b/assets/icons/file_icons/file.svg index 790a5f18d7..3c72bd3320 100644 --- a/assets/icons/file_icons/file.svg +++ b/assets/icons/file_icons/file.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/file_icons/folder.svg b/assets/icons/file_icons/folder.svg index e40613000d..a76dc63d1a 100644 --- a/assets/icons/file_icons/folder.svg +++ b/assets/icons/file_icons/folder.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/file_icons/folder_open.svg b/assets/icons/file_icons/folder_open.svg index 55231fb6ab..ef37f55f83 100644 --- a/assets/icons/file_icons/folder_open.svg +++ b/assets/icons/file_icons/folder_open.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/file_icons/font.svg b/assets/icons/file_icons/font.svg index 6f2b734b26..4cb01a28f2 100644 --- a/assets/icons/file_icons/font.svg +++ b/assets/icons/file_icons/font.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/file_icons/git.svg b/assets/icons/file_icons/git.svg index 2b36b0ffd3..197db2e9e6 100644 --- a/assets/icons/file_icons/git.svg +++ b/assets/icons/file_icons/git.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/assets/icons/file_icons/gleam.svg b/assets/icons/file_icons/gleam.svg index 0399bb4dd2..6a3dc2c96f 100644 --- a/assets/icons/file_icons/gleam.svg +++ b/assets/icons/file_icons/gleam.svg @@ -1,7 +1,7 @@ - - + + diff --git a/assets/icons/file_icons/graphql.svg b/assets/icons/file_icons/graphql.svg index e6c0368182..9688472599 100644 --- a/assets/icons/file_icons/graphql.svg +++ b/assets/icons/file_icons/graphql.svg @@ -1,6 +1,6 @@ - - + + diff --git a/assets/icons/file_icons/hash.svg b/assets/icons/file_icons/hash.svg index 77e6c60072..2241904266 100644 --- a/assets/icons/file_icons/hash.svg +++ b/assets/icons/file_icons/hash.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/assets/icons/file_icons/heroku.svg b/assets/icons/file_icons/heroku.svg index 732adf72cb..826a88646b 100644 --- a/assets/icons/file_icons/heroku.svg +++ b/assets/icons/file_icons/heroku.svg @@ -1,5 +1,5 @@ - + diff --git a/assets/icons/file_icons/html.svg b/assets/icons/file_icons/html.svg index 8832bcba3a..41f254dd68 100644 --- a/assets/icons/file_icons/html.svg +++ b/assets/icons/file_icons/html.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/file_icons/image.svg b/assets/icons/file_icons/image.svg index c89de1b128..75e64c0a43 100644 --- a/assets/icons/file_icons/image.svg +++ b/assets/icons/file_icons/image.svg @@ -1,7 +1,7 @@ - - - + + + diff --git a/assets/icons/file_icons/java.svg b/assets/icons/file_icons/java.svg index 70d2d10ed7..63ce6e768c 100644 --- a/assets/icons/file_icons/java.svg +++ b/assets/icons/file_icons/java.svg @@ -1,7 +1,7 @@ - - - - - + + + + + diff --git a/assets/icons/file_icons/kdl.svg b/assets/icons/file_icons/kdl.svg deleted file mode 100644 index 92d9f28428..0000000000 --- a/assets/icons/file_icons/kdl.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/icons/file_icons/lock.svg b/assets/icons/file_icons/lock.svg index 10ae33869a..6bfef249b4 100644 --- a/assets/icons/file_icons/lock.svg +++ b/assets/icons/file_icons/lock.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/file_icons/magnifying_glass.svg b/assets/icons/file_icons/magnifying_glass.svg index d0440d905c..75c3e76c80 100644 --- a/assets/icons/file_icons/magnifying_glass.svg +++ b/assets/icons/file_icons/magnifying_glass.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/file_icons/nix.svg b/assets/icons/file_icons/nix.svg index 215d58a035..879a4d76aa 100644 --- a/assets/icons/file_icons/nix.svg +++ b/assets/icons/file_icons/nix.svg @@ -1,8 +1,8 @@ - - - - - - + + + + + + diff --git a/assets/icons/file_icons/notebook.svg b/assets/icons/file_icons/notebook.svg index 968d5c5982..b72ebc3967 100644 --- a/assets/icons/file_icons/notebook.svg +++ b/assets/icons/file_icons/notebook.svg @@ -1,8 +1,8 @@ - - - - - + + + + + diff --git a/assets/icons/file_icons/package.svg b/assets/icons/file_icons/package.svg index 16bbccb2e6..12889e8084 100644 --- a/assets/icons/file_icons/package.svg +++ b/assets/icons/file_icons/package.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/file_icons/phoenix.svg b/assets/icons/file_icons/phoenix.svg index 5db68b4e44..b61b8beda7 100644 --- a/assets/icons/file_icons/phoenix.svg +++ b/assets/icons/file_icons/phoenix.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/file_icons/plus.svg b/assets/icons/file_icons/plus.svg index 3449da3ecd..f343d5dd87 100644 --- a/assets/icons/file_icons/plus.svg +++ b/assets/icons/file_icons/plus.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/file_icons/prettier.svg b/assets/icons/file_icons/prettier.svg index f01230c33c..835bd3a126 100644 --- a/assets/icons/file_icons/prettier.svg +++ b/assets/icons/file_icons/prettier.svg @@ -1,12 +1,12 @@ - - - - - - - - - - + + + + + + + + + + diff --git a/assets/icons/file_icons/project.svg b/assets/icons/file_icons/project.svg index 509cc5f4d0..86a15d41bc 100644 --- a/assets/icons/file_icons/project.svg +++ b/assets/icons/file_icons/project.svg @@ -1,5 +1,5 @@ - + diff --git a/assets/icons/file_icons/puppet.svg b/assets/icons/file_icons/puppet.svg deleted file mode 100644 index cdf903bc62..0000000000 --- a/assets/icons/file_icons/puppet.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/file_icons/python.svg b/assets/icons/file_icons/python.svg index b44fdc539d..de904d8e04 100644 --- a/assets/icons/file_icons/python.svg +++ b/assets/icons/file_icons/python.svg @@ -1,6 +1,6 @@ - - + + diff --git a/assets/icons/file_icons/replace.svg b/assets/icons/file_icons/replace.svg index 287328e82e..837cb23b66 100644 --- a/assets/icons/file_icons/replace.svg +++ b/assets/icons/file_icons/replace.svg @@ -1,5 +1,5 @@ - + diff --git a/assets/icons/file_icons/replace_next.svg b/assets/icons/file_icons/replace_next.svg index a9a9fc91f5..72511be70a 100644 --- a/assets/icons/file_icons/replace_next.svg +++ b/assets/icons/file_icons/replace_next.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/file_icons/rust.svg b/assets/icons/file_icons/rust.svg index 9e4dc57adb..5db753628a 100644 --- a/assets/icons/file_icons/rust.svg +++ b/assets/icons/file_icons/rust.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/file_icons/scala.svg b/assets/icons/file_icons/scala.svg index 0884cc96f4..9e89d1fa82 100644 --- a/assets/icons/file_icons/scala.svg +++ b/assets/icons/file_icons/scala.svg @@ -1,7 +1,7 @@ - + diff --git a/assets/icons/file_icons/settings.svg b/assets/icons/file_icons/settings.svg index d308135ff1..081d25bf48 100644 --- a/assets/icons/file_icons/settings.svg +++ b/assets/icons/file_icons/settings.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/file_icons/tcl.svg b/assets/icons/file_icons/tcl.svg index 1bd7c4a551..bb15b0f8e7 100644 --- a/assets/icons/file_icons/tcl.svg +++ b/assets/icons/file_icons/tcl.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/file_icons/toml.svg b/assets/icons/file_icons/toml.svg index ae31911d6a..9ab78af50f 100644 --- a/assets/icons/file_icons/toml.svg +++ b/assets/icons/file_icons/toml.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/file_icons/video.svg b/assets/icons/file_icons/video.svg index c249d4c82b..b96e359edb 100644 --- a/assets/icons/file_icons/video.svg +++ b/assets/icons/file_icons/video.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/file_icons/vue.svg b/assets/icons/file_icons/vue.svg index 1f993e90ef..1cbe08dff5 100644 --- a/assets/icons/file_icons/vue.svg +++ b/assets/icons/file_icons/vue.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/file_lock.svg b/assets/icons/file_lock.svg index 10ae33869a..6bfef249b4 100644 --- a/assets/icons/file_lock.svg +++ b/assets/icons/file_lock.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/file_markdown.svg b/assets/icons/file_markdown.svg deleted file mode 100644 index 26688a3db0..0000000000 --- a/assets/icons/file_markdown.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/file_rust.svg b/assets/icons/file_rust.svg index 9e4dc57adb..5db753628a 100644 --- a/assets/icons/file_rust.svg +++ b/assets/icons/file_rust.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/file_search.svg b/assets/icons/file_search.svg new file mode 100644 index 0000000000..ddf5b14770 --- /dev/null +++ b/assets/icons/file_search.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/file_text.svg b/assets/icons/file_text.svg new file mode 100644 index 0000000000..7c602f2ac7 --- /dev/null +++ b/assets/icons/file_text.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/file_text_filled.svg b/assets/icons/file_text_filled.svg deleted file mode 100644 index 15c81cca62..0000000000 --- a/assets/icons/file_text_filled.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/file_text_outlined.svg b/assets/icons/file_text_outlined.svg deleted file mode 100644 index d2e8897251..0000000000 --- a/assets/icons/file_text_outlined.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/assets/icons/file_toml.svg b/assets/icons/file_toml.svg index ae31911d6a..9ab78af50f 100644 --- a/assets/icons/file_toml.svg +++ b/assets/icons/file_toml.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/file_tree.svg b/assets/icons/file_tree.svg index baf0e26ce6..a140cd70b1 100644 --- a/assets/icons/file_tree.svg +++ b/assets/icons/file_tree.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/filter.svg b/assets/icons/filter.svg index 4aa14e93c0..7391fea132 100644 --- a/assets/icons/filter.svg +++ b/assets/icons/filter.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/flame.svg b/assets/icons/flame.svg index 89fc6cab1e..075e027a5c 100644 --- a/assets/icons/flame.svg +++ b/assets/icons/flame.svg @@ -1 +1 @@ - + diff --git a/assets/icons/folder.svg b/assets/icons/folder.svg index 35f4c1f8ac..1a40805a70 100644 --- a/assets/icons/folder.svg +++ b/assets/icons/folder.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/folder_open.svg b/assets/icons/folder_open.svg index 55231fb6ab..ef37f55f83 100644 --- a/assets/icons/folder_open.svg +++ b/assets/icons/folder_open.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/folder_search.svg b/assets/icons/folder_search.svg deleted file mode 100644 index 207ea5c10e..0000000000 --- a/assets/icons/folder_search.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/folder_x.svg b/assets/icons/folder_x.svg new file mode 100644 index 0000000000..b0f06f68eb --- /dev/null +++ b/assets/icons/folder_x.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/font.svg b/assets/icons/font.svg index 47633a58c9..861ab1a415 100644 --- a/assets/icons/font.svg +++ b/assets/icons/font.svg @@ -1 +1 @@ - + diff --git a/assets/icons/font_size.svg b/assets/icons/font_size.svg index 4286277bd9..cfba2deb6c 100644 --- a/assets/icons/font_size.svg +++ b/assets/icons/font_size.svg @@ -1 +1 @@ - + diff --git a/assets/icons/font_weight.svg b/assets/icons/font_weight.svg index 410f43ec6e..3ebbfa77bc 100644 --- a/assets/icons/font_weight.svg +++ b/assets/icons/font_weight.svg @@ -1 +1 @@ - + diff --git a/assets/icons/forward_arrow.svg b/assets/icons/forward_arrow.svg index e51796e554..0a7b71993f 100644 --- a/assets/icons/forward_arrow.svg +++ b/assets/icons/forward_arrow.svg @@ -1,4 +1 @@ - - - - + diff --git a/assets/icons/function.svg b/assets/icons/function.svg new file mode 100644 index 0000000000..5d0b9d58ef --- /dev/null +++ b/assets/icons/function.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/generic_maximize.svg b/assets/icons/generic_maximize.svg index f1d7da44ef..e44abd8f06 100644 --- a/assets/icons/generic_maximize.svg +++ b/assets/icons/generic_maximize.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/generic_restore.svg b/assets/icons/generic_restore.svg index d8a3d72bcd..3bf581f2cd 100644 --- a/assets/icons/generic_restore.svg +++ b/assets/icons/generic_restore.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/git_branch.svg b/assets/icons/git_branch.svg index fc6dcfe1b2..db6190a9c8 100644 --- a/assets/icons/git_branch.svg +++ b/assets/icons/git_branch.svg @@ -1 +1 @@ - + diff --git a/assets/icons/git_branch_alt.svg b/assets/icons/git_branch_alt.svg deleted file mode 100644 index cf40195d8b..0000000000 --- a/assets/icons/git_branch_alt.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/assets/icons/git_branch_small.svg b/assets/icons/git_branch_small.svg new file mode 100644 index 0000000000..22832d6fed --- /dev/null +++ b/assets/icons/git_branch_small.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/git_onboarding_bg.svg b/assets/icons/git_onboarding_bg.svg new file mode 100644 index 0000000000..18da0230a2 --- /dev/null +++ b/assets/icons/git_onboarding_bg.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/github.svg b/assets/icons/github.svg index 0a12c9b656..28148b9894 100644 --- a/assets/icons/github.svg +++ b/assets/icons/github.svg @@ -1 +1 @@ - + diff --git a/assets/icons/globe.svg b/assets/icons/globe.svg new file mode 100644 index 0000000000..545b83aa71 --- /dev/null +++ b/assets/icons/globe.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/hammer.svg b/assets/icons/hammer.svg new file mode 100644 index 0000000000..ccc0d30e3d --- /dev/null +++ b/assets/icons/hammer.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/hash.svg b/assets/icons/hash.svg index afc1f9c0b5..f685245ed3 100644 --- a/assets/icons/hash.svg +++ b/assets/icons/hash.svg @@ -1 +1,6 @@ - + + + + + + diff --git a/assets/icons/history_rerun.svg b/assets/icons/history_rerun.svg index e11e754318..9ade606b31 100644 --- a/assets/icons/history_rerun.svg +++ b/assets/icons/history_rerun.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/image.svg b/assets/icons/image.svg index e0d73d7621..4b17300f47 100644 --- a/assets/icons/image.svg +++ b/assets/icons/image.svg @@ -1 +1 @@ - + diff --git a/assets/icons/info.svg b/assets/icons/info.svg index c000f25867..f3d2e6644f 100644 --- a/assets/icons/info.svg +++ b/assets/icons/info.svg @@ -1,5 +1,5 @@ - - + + diff --git a/assets/icons/inlay_hint.svg b/assets/icons/inlay_hint.svg new file mode 100644 index 0000000000..c8e6bb2d36 --- /dev/null +++ b/assets/icons/inlay_hint.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/json.svg b/assets/icons/json.svg deleted file mode 100644 index af2f6c5dc0..0000000000 --- a/assets/icons/json.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/keyboard.svg b/assets/icons/keyboard.svg index 82791cda3f..8bdc054a65 100644 --- a/assets/icons/keyboard.svg +++ b/assets/icons/keyboard.svg @@ -1 +1 @@ - + diff --git a/assets/icons/knockouts/x_fg.svg b/assets/icons/knockouts/x_fg.svg index f459954f72..a3d47f1373 100644 --- a/assets/icons/knockouts/x_fg.svg +++ b/assets/icons/knockouts/x_fg.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/layout.svg b/assets/icons/layout.svg new file mode 100644 index 0000000000..79464013b1 --- /dev/null +++ b/assets/icons/layout.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/library.svg b/assets/icons/library.svg index fc7f5afcd2..95f8c710c8 100644 --- a/assets/icons/library.svg +++ b/assets/icons/library.svg @@ -1,6 +1 @@ - - - - - - + diff --git a/assets/icons/light_bulb.svg b/assets/icons/light_bulb.svg new file mode 100644 index 0000000000..61a8f04211 --- /dev/null +++ b/assets/icons/light_bulb.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/line_height.svg b/assets/icons/line_height.svg index 3929fc4080..904cfad8a8 100644 --- a/assets/icons/line_height.svg +++ b/assets/icons/line_height.svg @@ -1 +1,6 @@ - + + + + + + diff --git a/assets/icons/link.svg b/assets/icons/link.svg new file mode 100644 index 0000000000..4925bd8e00 --- /dev/null +++ b/assets/icons/link.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/list_collapse.svg b/assets/icons/list_collapse.svg index f18bc550b9..a0e0ed604d 100644 --- a/assets/icons/list_collapse.svg +++ b/assets/icons/list_collapse.svg @@ -1 +1 @@ - + diff --git a/assets/icons/list_todo.svg b/assets/icons/list_todo.svg index 709f26d89d..1f50219418 100644 --- a/assets/icons/list_todo.svg +++ b/assets/icons/list_todo.svg @@ -1 +1 @@ - + diff --git a/assets/icons/list_tree.svg b/assets/icons/list_tree.svg index de3e0f3a57..09872a60f7 100644 --- a/assets/icons/list_tree.svg +++ b/assets/icons/list_tree.svg @@ -1,7 +1,7 @@ - - - - - + + + + + diff --git a/assets/icons/list_x.svg b/assets/icons/list_x.svg index 0fa3bd68fb..683f38ab5d 100644 --- a/assets/icons/list_x.svg +++ b/assets/icons/list_x.svg @@ -1,7 +1,7 @@ - - - - - + + + + + diff --git a/assets/icons/load_circle.svg b/assets/icons/load_circle.svg index eecf099310..c4de36b1ff 100644 --- a/assets/icons/load_circle.svg +++ b/assets/icons/load_circle.svg @@ -1 +1 @@ - + diff --git a/assets/icons/location_edit.svg b/assets/icons/location_edit.svg index e342652eb1..de82e8db4e 100644 --- a/assets/icons/location_edit.svg +++ b/assets/icons/location_edit.svg @@ -1 +1 @@ - + diff --git a/assets/icons/lock_outlined.svg b/assets/icons/lock_outlined.svg index d69a245603..0bfd2fdc82 100644 --- a/assets/icons/lock_outlined.svg +++ b/assets/icons/lock_outlined.svg @@ -1,6 +1,6 @@ - - + + - + diff --git a/assets/icons/logo_96.svg b/assets/icons/logo_96.svg new file mode 100644 index 0000000000..dc98bb8bc2 --- /dev/null +++ b/assets/icons/logo_96.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/lsp_debug.svg b/assets/icons/lsp_debug.svg new file mode 100644 index 0000000000..aa49fcb6a2 --- /dev/null +++ b/assets/icons/lsp_debug.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/lsp_restart.svg b/assets/icons/lsp_restart.svg new file mode 100644 index 0000000000..dfc68e7a9e --- /dev/null +++ b/assets/icons/lsp_restart.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/lsp_stop.svg b/assets/icons/lsp_stop.svg new file mode 100644 index 0000000000..c6311d2155 --- /dev/null +++ b/assets/icons/lsp_stop.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/magnifying_glass.svg b/assets/icons/magnifying_glass.svg index 24f00bb51b..75c3e76c80 100644 --- a/assets/icons/magnifying_glass.svg +++ b/assets/icons/magnifying_glass.svg @@ -1,4 +1,3 @@ - - + diff --git a/assets/icons/mail_open.svg b/assets/icons/mail_open.svg new file mode 100644 index 0000000000..b857037b86 --- /dev/null +++ b/assets/icons/mail_open.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/maximize.svg b/assets/icons/maximize.svg index 7b6d26fed8..b3504b5701 100644 --- a/assets/icons/maximize.svg +++ b/assets/icons/maximize.svg @@ -1,6 +1 @@ - - - - - - + diff --git a/assets/icons/menu.svg b/assets/icons/menu.svg index f12ce47f7e..6598697ff8 100644 --- a/assets/icons/menu.svg +++ b/assets/icons/menu.svg @@ -1 +1 @@ - + diff --git a/assets/icons/menu_alt.svg b/assets/icons/menu_alt.svg index b9cc19e22f..ae3581ba01 100644 --- a/assets/icons/menu_alt.svg +++ b/assets/icons/menu_alt.svg @@ -1,3 +1,5 @@ - - + + + + diff --git a/assets/icons/menu_alt_temp.svg b/assets/icons/menu_alt_temp.svg deleted file mode 100644 index 87add13216..0000000000 --- a/assets/icons/menu_alt_temp.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/message_bubbles.svg b/assets/icons/message_bubbles.svg new file mode 100644 index 0000000000..03a6c7760c --- /dev/null +++ b/assets/icons/message_bubbles.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/mic.svg b/assets/icons/mic.svg index 000d135ea5..1d9c5bc9ed 100644 --- a/assets/icons/mic.svg +++ b/assets/icons/mic.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/mic_mute.svg b/assets/icons/mic_mute.svg index 8bc63be610..8c61ae2f1c 100644 --- a/assets/icons/mic_mute.svg +++ b/assets/icons/mic_mute.svg @@ -1,8 +1,8 @@ - - - - - - + + + + + + diff --git a/assets/icons/microscope.svg b/assets/icons/microscope.svg new file mode 100644 index 0000000000..2b3009a28b --- /dev/null +++ b/assets/icons/microscope.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/minimize.svg b/assets/icons/minimize.svg index 082ade47db..0451233cc9 100644 --- a/assets/icons/minimize.svg +++ b/assets/icons/minimize.svg @@ -1,6 +1 @@ - - - - - - + diff --git a/assets/icons/new_from_summary.svg b/assets/icons/new_from_summary.svg new file mode 100644 index 0000000000..3b61ca51a0 --- /dev/null +++ b/assets/icons/new_from_summary.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/new_text_thread.svg b/assets/icons/new_text_thread.svg new file mode 100644 index 0000000000..75afa934a0 --- /dev/null +++ b/assets/icons/new_text_thread.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/thread.svg b/assets/icons/new_thread.svg similarity index 90% rename from assets/icons/thread.svg rename to assets/icons/new_thread.svg index 496cf42e3a..8c2596a4c9 100644 --- a/assets/icons/thread.svg +++ b/assets/icons/new_thread.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/notepad.svg b/assets/icons/notepad.svg deleted file mode 100644 index 27fd35566e..0000000000 --- a/assets/icons/notepad.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/option.svg b/assets/icons/option.svg index 47201f7c67..9d54a6f34b 100644 --- a/assets/icons/option.svg +++ b/assets/icons/option.svg @@ -1,4 +1,3 @@ - - + diff --git a/assets/icons/panel_left.svg b/assets/icons/panel_left.svg new file mode 100644 index 0000000000..2eed26673e --- /dev/null +++ b/assets/icons/panel_left.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/panel_right.svg b/assets/icons/panel_right.svg new file mode 100644 index 0000000000..d29a4a519e --- /dev/null +++ b/assets/icons/panel_right.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/pencil.svg b/assets/icons/pencil.svg index c4d289e9c0..d90dcda10d 100644 --- a/assets/icons/pencil.svg +++ b/assets/icons/pencil.svg @@ -1,4 +1,3 @@ - - - + + diff --git a/assets/icons/pencil_unavailable.svg b/assets/icons/pencil_unavailable.svg deleted file mode 100644 index 4241d766ac..0000000000 --- a/assets/icons/pencil_unavailable.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/assets/icons/person.svg b/assets/icons/person.svg index a1c29e4acb..93bee97a5f 100644 --- a/assets/icons/person.svg +++ b/assets/icons/person.svg @@ -1 +1,4 @@ - + + + + diff --git a/assets/icons/person_circle.svg b/assets/icons/person_circle.svg new file mode 100644 index 0000000000..7e22682e0e --- /dev/null +++ b/assets/icons/person_circle.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/phone_incoming.svg b/assets/icons/phone_incoming.svg new file mode 100644 index 0000000000..4577df47ad --- /dev/null +++ b/assets/icons/phone_incoming.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/pin.svg b/assets/icons/pin.svg index d23daff8b9..f3f50cc659 100644 --- a/assets/icons/pin.svg +++ b/assets/icons/pin.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/play.svg b/assets/icons/play.svg new file mode 100644 index 0000000000..2481bda7d6 --- /dev/null +++ b/assets/icons/play.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/play_outlined.svg b/assets/icons/play_alt.svg similarity index 70% rename from assets/icons/play_outlined.svg rename to assets/icons/play_alt.svg index ba1ea2693d..b327ab07b5 100644 --- a/assets/icons/play_outlined.svg +++ b/assets/icons/play_alt.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/play_bug.svg b/assets/icons/play_bug.svg new file mode 100644 index 0000000000..7d265dd42a --- /dev/null +++ b/assets/icons/play_bug.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/play_filled.svg b/assets/icons/play_filled.svg index 8075197ad2..387304ef04 100644 --- a/assets/icons/play_filled.svg +++ b/assets/icons/play_filled.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/plus.svg b/assets/icons/plus.svg index 8ac57d8cdd..e26d430320 100644 --- a/assets/icons/plus.svg +++ b/assets/icons/plus.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/pocket_knife.svg b/assets/icons/pocket_knife.svg new file mode 100644 index 0000000000..fb2d078e20 --- /dev/null +++ b/assets/icons/pocket_knife.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/power.svg b/assets/icons/power.svg index 29bd2127c5..787d1a3519 100644 --- a/assets/icons/power.svg +++ b/assets/icons/power.svg @@ -1 +1 @@ - + diff --git a/assets/icons/public.svg b/assets/icons/public.svg index 5659b5419f..38278cdaba 100644 --- a/assets/icons/public.svg +++ b/assets/icons/public.svg @@ -1 +1,3 @@ - + + + diff --git a/assets/icons/pull_request.svg b/assets/icons/pull_request.svg index 515462ab64..150a532cc6 100644 --- a/assets/icons/pull_request.svg +++ b/assets/icons/pull_request.svg @@ -1 +1 @@ - + diff --git a/assets/icons/quote.svg b/assets/icons/quote.svg index a958bc67f2..b970db1430 100644 --- a/assets/icons/quote.svg +++ b/assets/icons/quote.svg @@ -1 +1 @@ - + diff --git a/assets/icons/reader.svg b/assets/icons/reader.svg deleted file mode 100644 index f477f4f32d..0000000000 --- a/assets/icons/reader.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/refresh_title.svg b/assets/icons/refresh_title.svg index c9e670bfab..bd3657d48c 100644 --- a/assets/icons/refresh_title.svg +++ b/assets/icons/refresh_title.svg @@ -1 +1,5 @@ - + + + + + diff --git a/assets/icons/regex.svg b/assets/icons/regex.svg index 818c2ba360..1b24398cc1 100644 --- a/assets/icons/regex.svg +++ b/assets/icons/regex.svg @@ -1,4 +1,4 @@ - - - + + + diff --git a/assets/icons/repl_neutral.svg b/assets/icons/repl_neutral.svg index 2842e2c421..db647fe40b 100644 --- a/assets/icons/repl_neutral.svg +++ b/assets/icons/repl_neutral.svg @@ -1,6 +1,13 @@ - - - - + + + + + + + + + + + diff --git a/assets/icons/repl_off.svg b/assets/icons/repl_off.svg index 3018ceaf85..51ada0db46 100644 --- a/assets/icons/repl_off.svg +++ b/assets/icons/repl_off.svg @@ -1,11 +1,20 @@ - - - - - - - - - - + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/repl_pause.svg b/assets/icons/repl_pause.svg index 5a69a576c1..2ac327df3b 100644 --- a/assets/icons/repl_pause.svg +++ b/assets/icons/repl_pause.svg @@ -1,8 +1,15 @@ - - - - - - - + + + + + + + + + + + + + + diff --git a/assets/icons/repl_play.svg b/assets/icons/repl_play.svg index 0c8f4b0832..d23b899112 100644 --- a/assets/icons/repl_play.svg +++ b/assets/icons/repl_play.svg @@ -1,7 +1,14 @@ - - - - - - + + + + + + + + + + + + + diff --git a/assets/icons/replace.svg b/assets/icons/replace.svg index 287328e82e..837cb23b66 100644 --- a/assets/icons/replace.svg +++ b/assets/icons/replace.svg @@ -1,5 +1,5 @@ - + diff --git a/assets/icons/replace_next.svg b/assets/icons/replace_next.svg index a9a9fc91f5..72511be70a 100644 --- a/assets/icons/replace_next.svg +++ b/assets/icons/replace_next.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/rerun.svg b/assets/icons/rerun.svg index 1a03a01ae6..4d22f924f5 100644 --- a/assets/icons/rerun.svg +++ b/assets/icons/rerun.svg @@ -1 +1,7 @@ - + + + + + + + diff --git a/assets/icons/return.svg b/assets/icons/return.svg index c605eb6512..16cfeeda2e 100644 --- a/assets/icons/return.svg +++ b/assets/icons/return.svg @@ -1,4 +1 @@ - - - - + diff --git a/assets/icons/reveal.svg b/assets/icons/reveal.svg new file mode 100644 index 0000000000..ff5444d8f8 --- /dev/null +++ b/assets/icons/reveal.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/rotate_ccw.svg b/assets/icons/rotate_ccw.svg index cdfa8d0ab4..4eff13b94b 100644 --- a/assets/icons/rotate_ccw.svg +++ b/assets/icons/rotate_ccw.svg @@ -1 +1 @@ - + diff --git a/assets/icons/rotate_cw.svg b/assets/icons/rotate_cw.svg index 2adfa7f972..2098de38c2 100644 --- a/assets/icons/rotate_cw.svg +++ b/assets/icons/rotate_cw.svg @@ -1 +1,4 @@ - + + + + diff --git a/assets/icons/route.svg b/assets/icons/route.svg new file mode 100644 index 0000000000..7d2a5621ff --- /dev/null +++ b/assets/icons/route.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/save.svg b/assets/icons/save.svg new file mode 100644 index 0000000000..f83d035331 --- /dev/null +++ b/assets/icons/save.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/scissors.svg b/assets/icons/scissors.svg index a19580bd89..e7fb6005f4 100644 --- a/assets/icons/scissors.svg +++ b/assets/icons/scissors.svg @@ -1,3 +1 @@ - - - + diff --git a/assets/icons/screen.svg b/assets/icons/screen.svg index 4bcdf19528..4b686b58f9 100644 --- a/assets/icons/screen.svg +++ b/assets/icons/screen.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/scroll_text.svg b/assets/icons/scroll_text.svg new file mode 100644 index 0000000000..f066c8a84e --- /dev/null +++ b/assets/icons/scroll_text.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/search_selection.svg b/assets/icons/search_selection.svg new file mode 100644 index 0000000000..b970db1430 --- /dev/null +++ b/assets/icons/search_selection.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/select_all.svg b/assets/icons/select_all.svg index 4fa17dcf63..78c3ee6399 100644 --- a/assets/icons/select_all.svg +++ b/assets/icons/select_all.svg @@ -1 +1,5 @@ - + + + + + diff --git a/assets/icons/send.svg b/assets/icons/send.svg index 5ceeef2af4..0d6ad36341 100644 --- a/assets/icons/send.svg +++ b/assets/icons/send.svg @@ -1 +1,4 @@ - + + + + diff --git a/assets/icons/server.svg b/assets/icons/server.svg index 8d851d1328..a8b6ad92b3 100644 --- a/assets/icons/server.svg +++ b/assets/icons/server.svg @@ -1,6 +1,16 @@ - - - - - + + + + + diff --git a/assets/icons/settings.svg b/assets/icons/settings.svg index 33ac74f230..a82cf03398 100644 --- a/assets/icons/settings.svg +++ b/assets/icons/settings.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/settings_alt.svg b/assets/icons/settings_alt.svg new file mode 100644 index 0000000000..a5fb4171d5 --- /dev/null +++ b/assets/icons/settings_alt.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/shield_check.svg b/assets/icons/shield_check.svg deleted file mode 100644 index 43b52f43a8..0000000000 --- a/assets/icons/shield_check.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/shift.svg b/assets/icons/shift.svg index c38807d8b0..0232114777 100644 --- a/assets/icons/shift.svg +++ b/assets/icons/shift.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/slash.svg b/assets/icons/slash.svg index 1ebf01eb9f..792c405bb0 100644 --- a/assets/icons/slash.svg +++ b/assets/icons/slash.svg @@ -1,3 +1 @@ - - - + diff --git a/assets/icons/slash_square.svg b/assets/icons/slash_square.svg new file mode 100644 index 0000000000..8f269ddeb5 --- /dev/null +++ b/assets/icons/slash_square.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/sliders.svg b/assets/icons/sliders.svg index 20a6a367dc..8ab83055ee 100644 --- a/assets/icons/sliders.svg +++ b/assets/icons/sliders.svg @@ -1,8 +1,8 @@ - - - - - - + + + + + + diff --git a/assets/icons/sliders_alt.svg b/assets/icons/sliders_alt.svg new file mode 100644 index 0000000000..36c3feccfe --- /dev/null +++ b/assets/icons/sliders_alt.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/sliders_vertical.svg b/assets/icons/sliders_vertical.svg new file mode 100644 index 0000000000..ab61037a51 --- /dev/null +++ b/assets/icons/sliders_vertical.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/icons/snip.svg b/assets/icons/snip.svg new file mode 100644 index 0000000000..03ae4ce039 --- /dev/null +++ b/assets/icons/snip.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/space.svg b/assets/icons/space.svg index 0294c9bf1e..63718fb4aa 100644 --- a/assets/icons/space.svg +++ b/assets/icons/space.svg @@ -1,3 +1 @@ - - - + diff --git a/assets/icons/sparkle.svg b/assets/icons/sparkle.svg index 535c447723..f420f527f1 100644 --- a/assets/icons/sparkle.svg +++ b/assets/icons/sparkle.svg @@ -1 +1 @@ - + diff --git a/assets/icons/sparkle_alt.svg b/assets/icons/sparkle_alt.svg new file mode 100644 index 0000000000..d5c227b105 --- /dev/null +++ b/assets/icons/sparkle_alt.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/sparkle_filled.svg b/assets/icons/sparkle_filled.svg new file mode 100644 index 0000000000..96837f618d --- /dev/null +++ b/assets/icons/sparkle_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/speaker_loud.svg b/assets/icons/speaker_loud.svg new file mode 100644 index 0000000000..68982ee5e9 --- /dev/null +++ b/assets/icons/speaker_loud.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/spinner.svg b/assets/icons/spinner.svg new file mode 100644 index 0000000000..4f4034ae89 --- /dev/null +++ b/assets/icons/spinner.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/assets/icons/split.svg b/assets/icons/split.svg index b2be46a875..4c131466c2 100644 --- a/assets/icons/split.svg +++ b/assets/icons/split.svg @@ -1,5 +1,5 @@ - - - - + + + + diff --git a/assets/icons/split_alt.svg b/assets/icons/split_alt.svg index 2f99e1436f..3f7622701d 100644 --- a/assets/icons/split_alt.svg +++ b/assets/icons/split_alt.svg @@ -1 +1 @@ - + diff --git a/assets/icons/square_dot.svg b/assets/icons/square_dot.svg index 72b3273439..2c1d8afdcb 100644 --- a/assets/icons/square_dot.svg +++ b/assets/icons/square_dot.svg @@ -1,4 +1 @@ - - - - + diff --git a/assets/icons/square_minus.svg b/assets/icons/square_minus.svg index 5ba458e8b5..a9ab42c408 100644 --- a/assets/icons/square_minus.svg +++ b/assets/icons/square_minus.svg @@ -1,4 +1 @@ - - - - + diff --git a/assets/icons/square_plus.svg b/assets/icons/square_plus.svg index 063c7dbf82..8cbe3dc0e7 100644 --- a/assets/icons/square_plus.svg +++ b/assets/icons/square_plus.svg @@ -1,5 +1 @@ - - - - - + diff --git a/assets/icons/star.svg b/assets/icons/star.svg index b39638e386..fd1502ede8 100644 --- a/assets/icons/star.svg +++ b/assets/icons/star.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/star_filled.svg b/assets/icons/star_filled.svg index 16f64e5cb3..89b03ded29 100644 --- a/assets/icons/star_filled.svg +++ b/assets/icons/star_filled.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/stop.svg b/assets/icons/stop.svg index cc2bbe9207..6291a34c08 100644 --- a/assets/icons/stop.svg +++ b/assets/icons/stop.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/stop_filled.svg b/assets/icons/stop_filled.svg new file mode 100644 index 0000000000..caf40d197e --- /dev/null +++ b/assets/icons/stop_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/strikethrough.svg b/assets/icons/strikethrough.svg new file mode 100644 index 0000000000..d7d0905912 --- /dev/null +++ b/assets/icons/strikethrough.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/supermaven.svg b/assets/icons/supermaven.svg index af778c70b7..19837fbf56 100644 --- a/assets/icons/supermaven.svg +++ b/assets/icons/supermaven.svg @@ -1,8 +1,8 @@ - - - - - - - + + + + + + + diff --git a/assets/icons/supermaven_disabled.svg b/assets/icons/supermaven_disabled.svg index 25eea54cde..39ff8a6122 100644 --- a/assets/icons/supermaven_disabled.svg +++ b/assets/icons/supermaven_disabled.svg @@ -1 +1,15 @@ - + + + + + + + + + + + + + + + diff --git a/assets/icons/supermaven_error.svg b/assets/icons/supermaven_error.svg index a0a12e17c3..669322b97d 100644 --- a/assets/icons/supermaven_error.svg +++ b/assets/icons/supermaven_error.svg @@ -1,11 +1,11 @@ - - - - - - + + + + + + - + diff --git a/assets/icons/supermaven_init.svg b/assets/icons/supermaven_init.svg index 6851aad49d..b919d5559b 100644 --- a/assets/icons/supermaven_init.svg +++ b/assets/icons/supermaven_init.svg @@ -1,11 +1,11 @@ - - - - - - + + + + + + - + diff --git a/assets/icons/swatch_book.svg b/assets/icons/swatch_book.svg index b37d5df8c1..985994ffcf 100644 --- a/assets/icons/swatch_book.svg +++ b/assets/icons/swatch_book.svg @@ -1 +1 @@ - + diff --git a/assets/icons/tab.svg b/assets/icons/tab.svg index db93be4df5..49a3536bed 100644 --- a/assets/icons/tab.svg +++ b/assets/icons/tab.svg @@ -1,5 +1 @@ - - - - - + diff --git a/assets/icons/terminal_alt.svg b/assets/icons/terminal_alt.svg index d03c05423e..7afb89db21 100644 --- a/assets/icons/terminal_alt.svg +++ b/assets/icons/terminal_alt.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/terminal_ghost.svg b/assets/icons/terminal_ghost.svg deleted file mode 100644 index 7d0d0e068e..0000000000 --- a/assets/icons/terminal_ghost.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/text_snippet.svg b/assets/icons/text_snippet.svg index b8987546d3..255635de6a 100644 --- a/assets/icons/text_snippet.svg +++ b/assets/icons/text_snippet.svg @@ -1 +1 @@ - + diff --git a/assets/icons/text_thread.svg b/assets/icons/text_thread.svg deleted file mode 100644 index aa078c72a2..0000000000 --- a/assets/icons/text_thread.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/assets/icons/thread_from_summary.svg b/assets/icons/thread_from_summary.svg deleted file mode 100644 index 94ce9562da..0000000000 --- a/assets/icons/thread_from_summary.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/assets/icons/thumbs_down.svg b/assets/icons/thumbs_down.svg index a396ff14f6..2edc09acd1 100644 --- a/assets/icons/thumbs_down.svg +++ b/assets/icons/thumbs_down.svg @@ -1 +1,3 @@ - + + + diff --git a/assets/icons/thumbs_up.svg b/assets/icons/thumbs_up.svg index 73c859c355..ff4406034d 100644 --- a/assets/icons/thumbs_up.svg +++ b/assets/icons/thumbs_up.svg @@ -1 +1,3 @@ - + + + diff --git a/assets/icons/todo_complete.svg b/assets/icons/todo_complete.svg index 5bf70841a8..9fa2e818bb 100644 --- a/assets/icons/todo_complete.svg +++ b/assets/icons/todo_complete.svg @@ -1 +1,4 @@ - + + + + diff --git a/assets/icons/todo_pending.svg b/assets/icons/todo_pending.svg index e5e9776f11..dfb013b52b 100644 --- a/assets/icons/todo_pending.svg +++ b/assets/icons/todo_pending.svg @@ -1,10 +1,10 @@ - - - - - - - - + + + + + + + + diff --git a/assets/icons/todo_progress.svg b/assets/icons/todo_progress.svg index b4a3e8c50e..9b2ed7375d 100644 --- a/assets/icons/todo_progress.svg +++ b/assets/icons/todo_progress.svg @@ -1,11 +1,11 @@ - - - - - - - - - + + + + + + + + + diff --git a/assets/icons/tool_bulb.svg b/assets/icons/tool_bulb.svg new file mode 100644 index 0000000000..54d5ac5fd7 --- /dev/null +++ b/assets/icons/tool_bulb.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/tool_copy.svg b/assets/icons/tool_copy.svg index a497a5c9cb..e722d8a022 100644 --- a/assets/icons/tool_copy.svg +++ b/assets/icons/tool_copy.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/tool_delete_file.svg b/assets/icons/tool_delete_file.svg index e15c0cb568..3276f3d78e 100644 --- a/assets/icons/tool_delete_file.svg +++ b/assets/icons/tool_delete_file.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/tool_diagnostics.svg b/assets/icons/tool_diagnostics.svg index 414810628d..c659d96781 100644 --- a/assets/icons/tool_diagnostics.svg +++ b/assets/icons/tool_diagnostics.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/tool_folder.svg b/assets/icons/tool_folder.svg index 35f4c1f8ac..9d3ac299d2 100644 --- a/assets/icons/tool_folder.svg +++ b/assets/icons/tool_folder.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/tool_hammer.svg b/assets/icons/tool_hammer.svg index f725012cdf..e66173ce70 100644 --- a/assets/icons/tool_hammer.svg +++ b/assets/icons/tool_hammer.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/tool_notification.svg b/assets/icons/tool_notification.svg index 7903a3369a..7510b32040 100644 --- a/assets/icons/tool_notification.svg +++ b/assets/icons/tool_notification.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/tool_pencil.svg b/assets/icons/tool_pencil.svg index c4d289e9c0..b913015c08 100644 --- a/assets/icons/tool_pencil.svg +++ b/assets/icons/tool_pencil.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/tool_read.svg b/assets/icons/tool_read.svg index d22e9d8c7d..458cbb3660 100644 --- a/assets/icons/tool_read.svg +++ b/assets/icons/tool_read.svg @@ -1,7 +1,7 @@ - - - - - + + + + + diff --git a/assets/icons/tool_regex.svg b/assets/icons/tool_regex.svg index 818c2ba360..0432cd570f 100644 --- a/assets/icons/tool_regex.svg +++ b/assets/icons/tool_regex.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/tool_search.svg b/assets/icons/tool_search.svg index b225a1298e..4f2750cfa2 100644 --- a/assets/icons/tool_search.svg +++ b/assets/icons/tool_search.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/tool_terminal.svg b/assets/icons/tool_terminal.svg index 24da5e3a10..5154fa8e70 100644 --- a/assets/icons/tool_terminal.svg +++ b/assets/icons/tool_terminal.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/tool_think.svg b/assets/icons/tool_think.svg deleted file mode 100644 index 773f5e7fa7..0000000000 --- a/assets/icons/tool_think.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/tool_web.svg b/assets/icons/tool_web.svg index 288b54c432..6250a9f05a 100644 --- a/assets/icons/tool_web.svg +++ b/assets/icons/tool_web.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/trash.svg b/assets/icons/trash.svg index 4a9e9add02..b71035b99c 100644 --- a/assets/icons/trash.svg +++ b/assets/icons/trash.svg @@ -1,5 +1 @@ - - - - - + diff --git a/assets/icons/trash_alt.svg b/assets/icons/trash_alt.svg new file mode 100644 index 0000000000..6867b42147 --- /dev/null +++ b/assets/icons/trash_alt.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/triangle.svg b/assets/icons/triangle.svg index c36d382e73..0ecf071e24 100644 --- a/assets/icons/triangle.svg +++ b/assets/icons/triangle.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/triangle_right.svg b/assets/icons/triangle_right.svg index bb82d8e637..2c78a316f7 100644 --- a/assets/icons/triangle_right.svg +++ b/assets/icons/triangle_right.svg @@ -1,3 +1 @@ - - - + diff --git a/assets/icons/undo.svg b/assets/icons/undo.svg index c714b58747..907cc77195 100644 --- a/assets/icons/undo.svg +++ b/assets/icons/undo.svg @@ -1 +1 @@ - + diff --git a/assets/icons/update.svg b/assets/icons/update.svg new file mode 100644 index 0000000000..b529b2b08b --- /dev/null +++ b/assets/icons/update.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/user_check.svg b/assets/icons/user_check.svg index ee32a52590..e5f13feeb4 100644 --- a/assets/icons/user_check.svg +++ b/assets/icons/user_check.svg @@ -1 +1 @@ - + diff --git a/assets/icons/user_group.svg b/assets/icons/user_group.svg index 30d2e5a7ea..ac1f7bdc63 100644 --- a/assets/icons/user_group.svg +++ b/assets/icons/user_group.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/user_round_pen.svg b/assets/icons/user_round_pen.svg index e684fd1a20..e25bf10469 100644 --- a/assets/icons/user_round_pen.svg +++ b/assets/icons/user_round_pen.svg @@ -1 +1 @@ - + diff --git a/assets/icons/visible.svg b/assets/icons/visible.svg new file mode 100644 index 0000000000..0a7e65d60d --- /dev/null +++ b/assets/icons/visible.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/wand.svg b/assets/icons/wand.svg new file mode 100644 index 0000000000..a6704b1c42 --- /dev/null +++ b/assets/icons/wand.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/warning.svg b/assets/icons/warning.svg index 5af37dab9d..c48a575a90 100644 --- a/assets/icons/warning.svg +++ b/assets/icons/warning.svg @@ -1 +1 @@ - + diff --git a/assets/icons/whole_word.svg b/assets/icons/whole_word.svg index ce0d1606c8..beca4cbe82 100644 --- a/assets/icons/whole_word.svg +++ b/assets/icons/whole_word.svg @@ -1 +1,5 @@ - + + + + + diff --git a/assets/icons/x.svg b/assets/icons/x.svg new file mode 100644 index 0000000000..5d91a9edd9 --- /dev/null +++ b/assets/icons/x.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/x_circle.svg b/assets/icons/x_circle.svg index 8807e5fa1f..593629beee 100644 --- a/assets/icons/x_circle.svg +++ b/assets/icons/x_circle.svg @@ -1 +1,4 @@ - + + + + diff --git a/assets/icons/x_circle_filled.svg b/assets/icons/x_circle_filled.svg deleted file mode 100644 index 52215acda8..0000000000 --- a/assets/icons/x_circle_filled.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/zed_agent.svg b/assets/icons/zed_agent.svg deleted file mode 100644 index 0c80e22c51..0000000000 --- a/assets/icons/zed_agent.svg +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/icons/zed_assistant.svg b/assets/icons/zed_assistant.svg index 812277a100..d21252de8c 100644 --- a/assets/icons/zed_assistant.svg +++ b/assets/icons/zed_assistant.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/zed_assistant_filled.svg b/assets/icons/zed_assistant_filled.svg new file mode 100644 index 0000000000..8d16fd9849 --- /dev/null +++ b/assets/icons/zed_assistant_filled.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/zed_burn_mode.svg b/assets/icons/zed_burn_mode.svg index cad6ed666b..544368d8e0 100644 --- a/assets/icons/zed_burn_mode.svg +++ b/assets/icons/zed_burn_mode.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/zed_burn_mode_on.svg b/assets/icons/zed_burn_mode_on.svg index 10e0e42b13..94230b6fd6 100644 --- a/assets/icons/zed_burn_mode_on.svg +++ b/assets/icons/zed_burn_mode_on.svg @@ -1 +1,13 @@ - + + + + + + + + + + + + + diff --git a/assets/icons/zed_mcp_custom.svg b/assets/icons/zed_mcp_custom.svg index feff2d7d34..6410a26fca 100644 --- a/assets/icons/zed_mcp_custom.svg +++ b/assets/icons/zed_mcp_custom.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/zed_mcp_extension.svg b/assets/icons/zed_mcp_extension.svg index 00117efcf4..996e0c1920 100644 --- a/assets/icons/zed_mcp_extension.svg +++ b/assets/icons/zed_mcp_extension.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/zed_predict.svg b/assets/icons/zed_predict.svg index 605a0584d5..79fd8c8fc1 100644 --- a/assets/icons/zed_predict.svg +++ b/assets/icons/zed_predict.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/zed_predict_bg.svg b/assets/icons/zed_predict_bg.svg new file mode 100644 index 0000000000..1dccbb51af --- /dev/null +++ b/assets/icons/zed_predict_bg.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/zed_predict_down.svg b/assets/icons/zed_predict_down.svg index 79eef9b0b4..4532ad7e26 100644 --- a/assets/icons/zed_predict_down.svg +++ b/assets/icons/zed_predict_down.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/zed_predict_error.svg b/assets/icons/zed_predict_error.svg index 6f75326179..b2dc339fe9 100644 --- a/assets/icons/zed_predict_error.svg +++ b/assets/icons/zed_predict_error.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/zed_predict_up.svg b/assets/icons/zed_predict_up.svg index f77001e4bd..61ec143022 100644 --- a/assets/icons/zed_predict_up.svg +++ b/assets/icons/zed_predict_up.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/zed_x_copilot.svg b/assets/icons/zed_x_copilot.svg new file mode 100644 index 0000000000..d024678c50 --- /dev/null +++ b/assets/icons/zed_x_copilot.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/images/acp_grid.svg b/assets/images/acp_grid.svg deleted file mode 100644 index 8ebff8e1bc..0000000000 --- a/assets/images/acp_grid.svg +++ /dev/null @@ -1,1257 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/acp_logo.svg b/assets/images/acp_logo.svg deleted file mode 100644 index efaa46707b..0000000000 --- a/assets/images/acp_logo.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/images/acp_logo_serif.svg b/assets/images/acp_logo_serif.svg deleted file mode 100644 index 6bc359cf82..0000000000 --- a/assets/images/acp_logo_serif.svg +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/assets/images/pro_trial_stamp.svg b/assets/images/pro_trial_stamp.svg deleted file mode 100644 index a3f9095120..0000000000 --- a/assets/images/pro_trial_stamp.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/images/pro_user_stamp.svg b/assets/images/pro_user_stamp.svg deleted file mode 100644 index d037a9e833..0000000000 --- a/assets/images/pro_user_stamp.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 3cca560c00..8a8dbd8a90 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -16,6 +16,7 @@ "up": "menu::SelectPrevious", "enter": "menu::Confirm", "ctrl-enter": "menu::SecondaryConfirm", + "ctrl-escape": "menu::Cancel", "ctrl-c": "menu::Cancel", "escape": "menu::Cancel", "alt-shift-enter": "menu::Restart", @@ -40,7 +41,7 @@ "shift-f11": "debugger::StepOut", "f11": "zed::ToggleFullScreen", "ctrl-alt-z": "edit_prediction::RateCompletions", - "ctrl-alt-shift-i": "edit_prediction::ToggleMenu", + "ctrl-shift-i": "edit_prediction::ToggleMenu", "ctrl-alt-l": "lsp_tool::ToggleMenu" } }, @@ -120,7 +121,7 @@ "alt-g m": "git::OpenModifiedFiles", "menu": "editor::OpenContextMenu", "shift-f10": "editor::OpenContextMenu", - "ctrl-alt-shift-e": "editor::ToggleEditPrediction", + "ctrl-shift-e": "editor::ToggleEditPrediction", "f9": "editor::ToggleBreakpoint", "shift-f9": "editor::EditLogBreakpoint" } @@ -137,7 +138,7 @@ "find": "buffer_search::Deploy", "ctrl-f": "buffer_search::Deploy", "ctrl-h": "buffer_search::DeployReplace", - "ctrl->": "agent::QuoteSelection", + "ctrl->": "assistant::QuoteSelection", "ctrl-<": "assistant::InsertIntoEditor", "ctrl-alt-e": "editor::SelectEnclosingSymbol", "ctrl-shift-backspace": "editor::GoToPreviousChange", @@ -238,9 +239,8 @@ "ctrl-shift-a": "agent::ToggleContextPicker", "ctrl-shift-j": "agent::ToggleNavigationMenu", "ctrl-shift-i": "agent::ToggleOptionsMenu", - "ctrl-alt-shift-n": "agent::ToggleNewThreadMenu", "shift-alt-escape": "agent::ExpandMessageEditor", - "ctrl->": "agent::QuoteSelection", + "ctrl->": "assistant::QuoteSelection", "ctrl-alt-e": "agent::RemoveAllContext", "ctrl-shift-e": "project_panel::ToggleFocus", "ctrl-shift-enter": "agent::ContinueThread", @@ -326,23 +326,13 @@ } }, { - "context": "AcpThread > Editor && !use_modifier_to_send", + "context": "AcpThread > Editor", "use_key_equivalents": true, "bindings": { "enter": "agent::Chat", - "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" + "up": "agent::PreviousHistoryMessage", + "down": "agent::NextHistoryMessage", + "shift-ctrl-r": "agent::OpenAgentDiff" } }, { @@ -855,8 +845,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": "workspace::OpenWithSystem", - "alt-d": "project_panel::CompareMarkedFiles", + "ctrl-shift-enter": "project_panel::OpenWithSystem", "shift-find": "project_panel::NewSearchInDirectory", "ctrl-alt-shift-f": "project_panel::NewSearchInDirectory", "shift-down": "menu::SelectNext", @@ -1111,13 +1100,6 @@ "ctrl-enter": "menu::Confirm" } }, - { - "context": "OnboardingAiConfigurationModal", - "use_key_equivalents": true, - "bindings": { - "escape": "menu::Cancel" - } - }, { "context": "Diagnostics", "use_key_equivalents": true, @@ -1186,24 +1168,5 @@ "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-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" - } } ] diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index e72f4174ff..62ba187851 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -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->": "agent::QuoteSelection", + "cmd->": "assistant::QuoteSelection", "cmd-<": "assistant::InsertIntoEditor", "cmd-alt-e": "editor::SelectEnclosingSymbol", "alt-enter": "editor::OpenSelectionsInMultibuffer" @@ -279,9 +279,8 @@ "cmd-shift-a": "agent::ToggleContextPicker", "cmd-shift-j": "agent::ToggleNavigationMenu", "cmd-shift-i": "agent::ToggleOptionsMenu", - "cmd-alt-shift-n": "agent::ToggleNewThreadMenu", "shift-alt-escape": "agent::ExpandMessageEditor", - "cmd->": "agent::QuoteSelection", + "cmd->": "assistant::QuoteSelection", "cmd-alt-e": "agent::RemoveAllContext", "cmd-shift-e": "project_panel::ToggleFocus", "cmd-ctrl-b": "agent::ToggleBurnMode", @@ -379,23 +378,13 @@ } }, { - "context": "AcpThread > Editor && !use_modifier_to_send", + "context": "AcpThread > Editor", "use_key_equivalents": true, "bindings": { "enter": "agent::Chat", - "shift-ctrl-r": "agent::OpenAgentDiff", - "cmd-shift-y": "agent::KeepAll", - "cmd-shift-n": "agent::RejectAll" - } - }, - { - "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" + "up": "agent::PreviousHistoryMessage", + "down": "agent::NextHistoryMessage", + "shift-ctrl-r": "agent::OpenAgentDiff" } }, { @@ -915,8 +904,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": "workspace::OpenWithSystem", - "alt-d": "project_panel::CompareMarkedFiles", + "ctrl-shift-enter": "project_panel::OpenWithSystem", "cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }], "cmd-alt-shift-f": "project_panel::NewSearchInDirectory", "shift-down": "menu::SelectNext", @@ -1214,13 +1202,6 @@ "cmd-enter": "menu::Confirm" } }, - { - "context": "OnboardingAiConfigurationModal", - "use_key_equivalents": true, - "bindings": { - "escape": "menu::Cancel" - } - }, { "context": "Diagnostics", "use_key_equivalents": true, @@ -1289,24 +1270,5 @@ "up": "menu::SelectPrevious", "down": "menu::SelectNext" } - }, - { - "context": "Onboarding", - "use_key_equivalents": true, - "bindings": { - "cmd-1": "onboarding::ActivateBasicsPage", - "cmd-2": "onboarding::ActivateEditingPage", - "cmd-3": "onboarding::ActivateAISetupPage", - "cmd-escape": "onboarding::Finish", - "alt-tab": "onboarding::SignIn", - "alt-shift-a": "onboarding::OpenAccount" - } - }, - { - "context": "InvalidBuffer", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-enter": "workspace::OpenWithSystem" - } } ] diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json deleted file mode 100644 index c7a6c3149c..0000000000 --- a/assets/keymaps/default-windows.json +++ /dev/null @@ -1,1260 +0,0 @@ -[ - // 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", - "open": "workspace::Open", - "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", - "ctrl-delete": "editor::DeleteToNextWordEnd", - "cut": "editor::Cut", - "shift-delete": "editor::Cut", - "ctrl-x": "editor::Cut", - "copy": "editor::Copy", - "ctrl-insert": "editor::Copy", - "ctrl-c": "editor::Copy", - "paste": "editor::Paste", - "shift-insert": "editor::Paste", - "ctrl-v": "editor::Paste", - "undo": "editor::Undo", - "ctrl-z": "editor::Undo", - "redo": "editor::Redo", - "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::NewlineAbove", - "ctrl-shift-enter": "editor::NewlineBelow", - "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-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": { - "copy": "markdown::Copy", - "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", - "save": "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" - } - }, - { - "context": "AgentPanel > NavigationMenu", - "use_key_equivalents": true, - "bindings": { - "shift-backspace": "agent::DeleteRecentlyOpenThread" - } - }, - { - "context": "AgentPanel > Markdown", - "use_key_equivalents": true, - "bindings": { - "copy": "markdown::CopyAsMarkdown", - "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 > Editor", - "use_key_equivalents": true, - "bindings": { - "enter": "agent::Chat", - "ctrl-shift-r": "agent::OpenAgentDiff", - "ctrl-shift-y": "agent::KeepAll", - "ctrl-shift-n": "agent::RejectAll" - } - }, - { - "context": "ThreadHistory", - "use_key_equivalents": true, - "bindings": { - "backspace": "agent::RemoveSelectedThread" - } - }, - { - "context": "PromptLibrary", - "use_key_equivalents": true, - "bindings": { - "new": "rules_library::NewRule", - "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", - "find": "search::FocusSearch", - "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", - "shift-find": "search::FocusSearch", - "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", - "shift-find": "project_search::ToggleFocus", - "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-find": "project_search::ToggleFilters", - "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": { - "alt-open": ["projects::OpenRecent", { "create_new_window": false }], - // 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 }], - "shift-alt-open": ["projects::OpenRemote", { "from_existing_connection": false, "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", - "save": "workspace::Save", - "ctrl-s": "workspace::Save", - "ctrl-k ctrl-shift-s": "workspace::SaveWithoutFormat", - "shift-save": "workspace::SaveAs", - "ctrl-shift-s": "workspace::SaveAs", - "new": "workspace::NewFile", - "ctrl-n": "workspace::NewFile", - "shift-new": "workspace::NewWindow", - "ctrl-shift-n": "workspace::NewWindow", - "ctrl-`": "terminal_panel::ToggleFocus", - "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 }], - "shift-find": "pane::DeploySearch", - "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", - "alt-save": "workspace::SaveAll", - "ctrl-k s": "workspace::SaveAll", - "ctrl-k m": "language_selector::Toggle", - "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", - "alt-copy": "outline_panel::CopyPath", - "shift-alt-c": "outline_panel::CopyPath", - "shift-alt-copy": "workspace::CopyRelativePath", - "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", - "new": "project_panel::NewFile", - "ctrl-n": "project_panel::NewFile", - "alt-new": "project_panel::NewDirectory", - "alt-n": "project_panel::NewDirectory", - "cut": "project_panel::Cut", - "ctrl-x": "project_panel::Cut", - "copy": "project_panel::Copy", - "ctrl-insert": "project_panel::Copy", - "ctrl-c": "project_panel::Copy", - "paste": "project_panel::Paste", - "shift-insert": "project_panel::Paste", - "ctrl-v": "project_panel::Paste", - "alt-copy": "project_panel::CopyPath", - "shift-alt-c": "project_panel::CopyPath", - "shift-alt-copy": "workspace::CopyRelativePath", - "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", - "shift-find": "project_panel::NewSearchInDirectory", - "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": "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", - "copy": "terminal::Copy", - "ctrl-insert": "terminal::Copy", - "ctrl-shift-c": "terminal::Copy", - "paste": "terminal::Paste", - "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", - "find": "buffer_search::Deploy", - "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-find": "keymap_editor::ToggleKeystrokeSearch", - "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" - } - } -] diff --git a/assets/keymaps/linux/cursor.json b/assets/keymaps/linux/cursor.json index 2e27158e11..1c381b0cf0 100644 --- a/assets/keymaps/linux/cursor.json +++ b/assets/keymaps/linux/cursor.json @@ -17,8 +17,8 @@ "bindings": { "ctrl-i": "agent::ToggleFocus", "ctrl-shift-i": "agent::ToggleFocus", - "ctrl-shift-l": "agent::QuoteSelection", // In cursor uses "Ask" mode - "ctrl-l": "agent::QuoteSelection", // In cursor uses "Agent" mode + "ctrl-shift-l": "assistant::QuoteSelection", // In cursor uses "Ask" mode + "ctrl-l": "assistant::QuoteSelection", // In cursor uses "Agent" mode "ctrl-k": "assistant::InlineAssist", "ctrl-shift-k": "assistant::InsertIntoEditor" } diff --git a/assets/keymaps/linux/emacs.json b/assets/keymaps/linux/emacs.json index 62910e297b..0ff3796f03 100755 --- a/assets/keymaps/linux/emacs.json +++ b/assets/keymaps/linux/emacs.json @@ -38,7 +38,6 @@ "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 diff --git a/assets/keymaps/linux/jetbrains.json b/assets/keymaps/linux/jetbrains.json index 3df1243fed..9bc1f24bfb 100644 --- a/assets/keymaps/linux/jetbrains.json +++ b/assets/keymaps/linux/jetbrains.json @@ -166,7 +166,7 @@ { "context": "Diagnostics > Editor", "bindings": { "alt-6": "pane::CloseActiveItem" } }, { "context": "OutlinePanel", "bindings": { "alt-7": "workspace::CloseActiveDock" } }, { - "context": "Dock || Workspace || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)", + "context": "Dock || Workspace || Terminal || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)", "bindings": { "escape": "editor::ToggleFocus", "shift-escape": "workspace::CloseActiveDock" diff --git a/assets/keymaps/macos/cursor.json b/assets/keymaps/macos/cursor.json index 1d723bd75b..fdf9c437cf 100644 --- a/assets/keymaps/macos/cursor.json +++ b/assets/keymaps/macos/cursor.json @@ -17,8 +17,8 @@ "bindings": { "cmd-i": "agent::ToggleFocus", "cmd-shift-i": "agent::ToggleFocus", - "cmd-shift-l": "agent::QuoteSelection", // In cursor uses "Ask" mode - "cmd-l": "agent::QuoteSelection", // In cursor uses "Agent" mode + "cmd-shift-l": "assistant::QuoteSelection", // In cursor uses "Ask" mode + "cmd-l": "assistant::QuoteSelection", // In cursor uses "Agent" mode "cmd-k": "assistant::InlineAssist", "cmd-shift-k": "assistant::InsertIntoEditor" } diff --git a/assets/keymaps/macos/emacs.json b/assets/keymaps/macos/emacs.json index 62910e297b..0ff3796f03 100755 --- a/assets/keymaps/macos/emacs.json +++ b/assets/keymaps/macos/emacs.json @@ -38,7 +38,6 @@ "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 diff --git a/assets/keymaps/macos/jetbrains.json b/assets/keymaps/macos/jetbrains.json index 66962811f4..b1cd51a338 100644 --- a/assets/keymaps/macos/jetbrains.json +++ b/assets/keymaps/macos/jetbrains.json @@ -167,7 +167,7 @@ { "context": "Diagnostics > Editor", "bindings": { "cmd-6": "pane::CloseActiveItem" } }, { "context": "OutlinePanel", "bindings": { "cmd-7": "workspace::CloseActiveDock" } }, { - "context": "Dock || Workspace || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)", + "context": "Dock || Workspace || Terminal || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)", "bindings": { "escape": "editor::ToggleFocus", "shift-escape": "workspace::CloseActiveDock" diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 67add61bd3..6458ac1510 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -58,8 +58,6 @@ "[ space": "vim::InsertEmptyLineAbove", "[ e": "editor::MoveLineUp", "] e": "editor::MoveLineDown", - "[ f": "workspace::FollowNextCollaborator", - "] f": "workspace::FollowNextCollaborator", // Word motions "w": "vim::NextWordStart", @@ -335,14 +333,10 @@ "ctrl-x ctrl-c": "editor::ShowEditPrediction", // zed specific "ctrl-x ctrl-l": "editor::ToggleCodeActions", // zed specific "ctrl-x ctrl-z": "editor::Cancel", - "ctrl-x ctrl-e": "vim::LineDown", - "ctrl-x ctrl-y": "vim::LineUp", "ctrl-w": "editor::DeleteToPreviousWordStart", "ctrl-u": "editor::DeleteToBeginningOfLine", "ctrl-t": "vim::Indent", "ctrl-d": "vim::Outdent", - "ctrl-y": "vim::InsertFromAbove", - "ctrl-e": "vim::InsertFromBelow", "ctrl-k": ["vim::PushDigraph", {}], "ctrl-v": ["vim::PushLiteral", {}], "ctrl-shift-v": "editor::Paste", // note: this is *very* similar to ctrl-v in vim, but ctrl-shift-v on linux is the typical shortcut for paste when ctrl-v is already in use. @@ -392,7 +386,7 @@ "right": "vim::WrappingRight", "h": "vim::WrappingLeft", "l": "vim::WrappingRight", - "y": "vim::HelixYank", + "y": "editor::Copy", "alt-;": "vim::OtherEnd", "ctrl-r": "vim::Redo", "f": ["vim::PushFindForward", { "before": false, "multiline": true }], @@ -409,7 +403,6 @@ "g w": "vim::PushRewrap", "insert": "vim::InsertBefore", "alt-.": "vim::RepeatFind", - "alt-s": ["editor::SplitSelectionIntoLines", { "keep_selections": true }], // tree-sitter related commands "[ x": "editor::SelectLargerSyntaxNode", "] x": "editor::SelectSmallerSyntaxNode", @@ -428,13 +421,11 @@ "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", - "shift-r": "editor::Paste", "x": "editor::SelectLine", "shift-x": "editor::SelectLine", "%": "editor::SelectAll", @@ -821,8 +812,7 @@ "v": "project_panel::OpenPermanent", "p": "project_panel::Open", "x": "project_panel::RevealInFileManager", - "s": "workspace::OpenWithSystem", - "z d": "project_panel::CompareMarkedFiles", + "s": "project_panel::OpenWithSystem", "] c": "project_panel::SelectNextGitEntry", "[ c": "project_panel::SelectPrevGitEntry", "] d": "project_panel::SelectNextDiagnostic", diff --git a/assets/settings/default.json b/assets/settings/default.json index 804198090f..4734b5d118 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -28,9 +28,7 @@ "edit_prediction_provider": "zed" }, // The name of a font to use for rendering text in the editor - // ".ZedMono" currently aliases to Lilex - // but this may change in the future. - "buffer_font_family": ".ZedMono", + "buffer_font_family": "Zed Plex Mono", // Set the buffer text's font fallbacks, this will be merged with // the platform's default fallbacks. "buffer_font_fallbacks": null, @@ -56,9 +54,7 @@ "buffer_line_height": "comfortable", // The name of a font to use for rendering text in the UI // You can set this to ".SystemUIFont" to use the system font - // ".ZedSans" currently aliases to "IBM Plex Sans", but this may - // change in the future - "ui_font_family": ".ZedSans", + "ui_font_family": "Zed Plex Sans", // Set the UI's font fallbacks, this will be merged with the platform's // default font fallbacks. "ui_font_fallbacks": null, @@ -71,8 +67,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. Falls back to the UI font size if unset. - "agent_font_size": null, + // The default font size for text in the agent panel + "agent_font_size": 16, // How much to fade out unused code. "unnecessary_code_fade": 0.3, // Active pane styling settings. @@ -86,10 +82,10 @@ // Layout mode of the bottom dock. Defaults to "contained" // choices: contained, full, left_aligned, right_aligned "bottom_dock_layout": "contained", - // The direction that you want to split panes horizontally. Defaults to "down" - "pane_split_direction_horizontal": "down", - // The direction that you want to split panes vertically. Defaults to "right" - "pane_split_direction_vertical": "right", + // The direction that you want to split panes horizontally. Defaults to "up" + "pane_split_direction_horizontal": "up", + // The direction that you want to split panes vertically. Defaults to "left" + "pane_split_direction_vertical": "left", // Centered layout related settings. "centered_layout": { // The relative width of the left padding of the central pane from the @@ -162,12 +158,6 @@ // 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, @@ -292,8 +282,6 @@ // 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. @@ -608,8 +596,6 @@ // when a corresponding project entry becomes active. // Gitignored entries are never auto revealed. "auto_reveal_entries": true, - // Whether the project panel should open on startup. - "starts_open": true, // Whether to fold directories automatically and show compact folders // (e.g. "a/b/c" ) when a directory has only one subdirectory inside. "auto_fold_dirs": true, @@ -653,8 +639,6 @@ // "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 }, @@ -725,7 +709,7 @@ // Can be 'never', 'always', or 'when_in_call', // or a boolean (interpreted as 'never'/'always'). "button": "when_in_call", - // Where to dock the chat panel. Can be 'left' or 'right'. + // Where to the chat panel. Can be 'left' or 'right'. "dock": "right", // Default width of the chat panel. "default_width": 240 @@ -733,7 +717,7 @@ "git_panel": { // Whether to show the git panel button in the status bar. "button": true, - // Where to dock the git panel. Can be 'left' or 'right'. + // Where to show the git panel. Can be 'left' or 'right'. "dock": "left", // Default width of the git panel. "default_width": 360, @@ -897,6 +881,11 @@ }, // 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. @@ -1141,6 +1130,11 @@ // 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 @@ -1177,9 +1171,6 @@ // Sets a delay after which the inline blame information is shown. // Delay is restarted with every cursor movement. "delay_ms": 0, - // The amount of padding between the end of the source line and the start - // of the inline blame in units of em widths. - "padding": 7, // Whether or not to display the git commit summary on the same line. "show_commit_summary": false, // The minimum column number to show the inline blame information at @@ -1214,18 +1205,7 @@ // Any addition to this list will be merged with the default list. // Globs are matched relative to the worktree root, // except when starting with a slash (/) or equivalent in Windows. - "disabled_globs": [ - "**/.env*", - "**/*.pem", - "**/*.key", - "**/*.cert", - "**/*.crt", - "**/.dev.vars", - "**/secrets.yml", - "**/.zed/settings.json", // zed project settings - "/**/zed/settings.json", // zed user settings - "/**/zed/keymap.json" - ], + "disabled_globs": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/.dev.vars", "**/secrets.yml"], // When to show edit predictions previews in buffer. // This setting takes two possible values: // 1. Display predictions inline when there are no language server completions available. @@ -1253,13 +1233,6 @@ // 2. hour24 "hour_format": "hour12" }, - // Status bar-related settings. - "status_bar": { - // Whether to show the active language button in the status bar. - "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": { // What shell to use when opening a terminal. May take 3 values: @@ -1408,7 +1381,7 @@ // "font_size": 15, // Set the terminal's font family. If this option is not included, // the terminal will default to matching the buffer's font family. - // "font_family": ".ZedMono", + // "font_family": "Zed Plex Mono", // Set the terminal's font fallbacks. If this option is not included, // the terminal will default to matching the buffer's font fallbacks. // This will be merged with the platform's default font fallbacks @@ -1506,11 +1479,6 @@ // // 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 @@ -1637,9 +1605,6 @@ "allowed": true } }, - "Kotlin": { - "language_servers": ["kotlin-language-server", "!kotlin-lsp", "..."] - }, "LaTeX": { "formatter": "language_server", "language_servers": ["texlab", "..."], @@ -1653,6 +1618,9 @@ "use_on_type_format": false, "allow_rewrap": "anywhere", "soft_wrap": "editor_width", + "completions": { + "words": "disabled" + }, "prettier": { "allowed": true } @@ -1666,6 +1634,9 @@ } }, "Plain Text": { + "completions": { + "words": "disabled" + }, "allow_rewrap": "anywhere" }, "Python": { diff --git a/assets/settings/initial_tasks.json b/assets/settings/initial_tasks.json index 5cead67b6d..a79c550671 100644 --- a/assets/settings/initial_tasks.json +++ b/assets/settings/initial_tasks.json @@ -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": [] } ] diff --git a/assets/themes/ayu/ayu.json b/assets/themes/ayu/ayu.json index 0ffbb9f61e..f9f8720729 100644 --- a/assets/themes/ayu/ayu.json +++ b/assets/themes/ayu/ayu.json @@ -93,7 +93,7 @@ "terminal.ansi.bright_cyan": "#4c806fff", "terminal.ansi.dim_cyan": "#cbf2e4ff", "terminal.ansi.white": "#bfbdb6ff", - "terminal.ansi.bright_white": "#fafafaff", + "terminal.ansi.bright_white": "#bfbdb6ff", "terminal.ansi.dim_white": "#787876ff", "link_text.hover": "#5ac1feff", "conflict": "#feb454ff", @@ -479,7 +479,7 @@ "terminal.ansi.bright_cyan": "#ace0cbff", "terminal.ansi.dim_cyan": "#2a5f4aff", "terminal.ansi.white": "#fcfcfcff", - "terminal.ansi.bright_white": "#ffffffff", + "terminal.ansi.bright_white": "#fcfcfcff", "terminal.ansi.dim_white": "#bcbec0ff", "link_text.hover": "#3b9ee5ff", "conflict": "#f1ad49ff", @@ -865,7 +865,7 @@ "terminal.ansi.bright_cyan": "#4c806fff", "terminal.ansi.dim_cyan": "#cbf2e4ff", "terminal.ansi.white": "#cccac2ff", - "terminal.ansi.bright_white": "#fafafaff", + "terminal.ansi.bright_white": "#cccac2ff", "terminal.ansi.dim_white": "#898a8aff", "link_text.hover": "#72cffeff", "conflict": "#fecf72ff", diff --git a/assets/themes/gruvbox/gruvbox.json b/assets/themes/gruvbox/gruvbox.json index f0f0358b76..459825c733 100644 --- a/assets/themes/gruvbox/gruvbox.json +++ b/assets/themes/gruvbox/gruvbox.json @@ -94,7 +94,7 @@ "terminal.ansi.bright_cyan": "#45603eff", "terminal.ansi.dim_cyan": "#c7dfbdff", "terminal.ansi.white": "#fbf1c7ff", - "terminal.ansi.bright_white": "#ffffffff", + "terminal.ansi.bright_white": "#fbf1c7ff", "terminal.ansi.dim_white": "#b0a189ff", "link_text.hover": "#83a598ff", "version_control.added": "#b7bb26ff", @@ -494,7 +494,7 @@ "terminal.ansi.bright_cyan": "#45603eff", "terminal.ansi.dim_cyan": "#c7dfbdff", "terminal.ansi.white": "#fbf1c7ff", - "terminal.ansi.bright_white": "#ffffffff", + "terminal.ansi.bright_white": "#fbf1c7ff", "terminal.ansi.dim_white": "#b0a189ff", "link_text.hover": "#83a598ff", "version_control.added": "#b7bb26ff", @@ -894,7 +894,7 @@ "terminal.ansi.bright_cyan": "#45603eff", "terminal.ansi.dim_cyan": "#c7dfbdff", "terminal.ansi.white": "#fbf1c7ff", - "terminal.ansi.bright_white": "#ffffffff", + "terminal.ansi.bright_white": "#fbf1c7ff", "terminal.ansi.dim_white": "#b0a189ff", "link_text.hover": "#83a598ff", "version_control.added": "#b7bb26ff", @@ -1294,7 +1294,7 @@ "terminal.ansi.bright_cyan": "#9fbca8ff", "terminal.ansi.dim_cyan": "#253e2eff", "terminal.ansi.white": "#fbf1c7ff", - "terminal.ansi.bright_white": "#ffffffff", + "terminal.ansi.bright_white": "#fbf1c7ff", "terminal.ansi.dim_white": "#b0a189ff", "link_text.hover": "#0b6678ff", "version_control.added": "#797410ff", @@ -1694,7 +1694,7 @@ "terminal.ansi.bright_cyan": "#9fbca8ff", "terminal.ansi.dim_cyan": "#253e2eff", "terminal.ansi.white": "#f9f5d7ff", - "terminal.ansi.bright_white": "#ffffffff", + "terminal.ansi.bright_white": "#f9f5d7ff", "terminal.ansi.dim_white": "#b0a189ff", "link_text.hover": "#0b6678ff", "version_control.added": "#797410ff", @@ -2094,7 +2094,7 @@ "terminal.ansi.bright_cyan": "#9fbca8ff", "terminal.ansi.dim_cyan": "#253e2eff", "terminal.ansi.white": "#f2e5bcff", - "terminal.ansi.bright_white": "#ffffffff", + "terminal.ansi.bright_white": "#f2e5bcff", "terminal.ansi.dim_white": "#b0a189ff", "link_text.hover": "#0b6678ff", "version_control.added": "#797410ff", diff --git a/assets/themes/one/one.json b/assets/themes/one/one.json index 33f6d3c622..384ad28272 100644 --- a/assets/themes/one/one.json +++ b/assets/themes/one/one.json @@ -86,14 +86,14 @@ "terminal.ansi.blue": "#74ade8ff", "terminal.ansi.bright_blue": "#385378ff", "terminal.ansi.dim_blue": "#bed5f4ff", - "terminal.ansi.magenta": "#b477cfff", - "terminal.ansi.bright_magenta": "#d6b4e4ff", - "terminal.ansi.dim_magenta": "#612a79ff", + "terminal.ansi.magenta": "#be5046ff", + "terminal.ansi.bright_magenta": "#5e2b26ff", + "terminal.ansi.dim_magenta": "#e6a79eff", "terminal.ansi.cyan": "#6eb4bfff", "terminal.ansi.bright_cyan": "#3a565bff", "terminal.ansi.dim_cyan": "#b9d9dfff", "terminal.ansi.white": "#dce0e5ff", - "terminal.ansi.bright_white": "#fafafaff", + "terminal.ansi.bright_white": "#dce0e5ff", "terminal.ansi.dim_white": "#575d65ff", "link_text.hover": "#74ade8ff", "version_control.added": "#27a657ff", @@ -468,7 +468,7 @@ "terminal.bright_foreground": "#242529ff", "terminal.dim_foreground": "#fafafaff", "terminal.ansi.black": "#242529ff", - "terminal.ansi.bright_black": "#747579ff", + "terminal.ansi.bright_black": "#242529ff", "terminal.ansi.dim_black": "#97979aff", "terminal.ansi.red": "#d36151ff", "terminal.ansi.bright_red": "#f0b0a4ff", @@ -489,7 +489,7 @@ "terminal.ansi.bright_cyan": "#a3bedaff", "terminal.ansi.dim_cyan": "#254058ff", "terminal.ansi.white": "#fafafaff", - "terminal.ansi.bright_white": "#ffffffff", + "terminal.ansi.bright_white": "#fafafaff", "terminal.ansi.dim_white": "#aaaaaaff", "link_text.hover": "#5c78e2ff", "version_control.added": "#27a657ff", diff --git a/crates/acp_thread/Cargo.toml b/crates/acp_thread/Cargo.toml index eab756db51..011f26f364 100644 --- a/crates/acp_thread/Cargo.toml +++ b/crates/acp_thread/Cargo.toml @@ -13,44 +13,35 @@ path = "src/acp_thread.rs" doctest = false [features] -test-support = ["gpui/test-support", "project/test-support", "dep:parking_lot"] +test-support = ["gpui/test-support", "project/test-support"] [dependencies] -action_log.workspace = true agent-client-protocol.workspace = true +agentic-coding-protocol.workspace = true anyhow.workspace = true +assistant_tool.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 } project.workspace = true -prompt_store.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true smol.workspace = true -terminal.workspace = true ui.workspace = true -url.workspace = true util.workspace = true -uuid.workspace = true -watch.workspace = true workspace-hack.workspace = true [dev-dependencies] +async-pipe.workspace = true env_logger.workspace = true gpui = { workspace = true, "features" = ["test-support"] } indoc.workspace = true -parking_lot.workspace = true project = { workspace = true, "features" = ["test-support"] } -rand.workspace = true tempfile.workspace = true util.workspace = true settings.workspace = true diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 4ded647a74..7203580410 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -1,68 +1,87 @@ mod connection; -mod diff; -mod mention; -mod terminal; - -use collections::HashSet; +mod old_acp_support; pub use connection::*; -pub use diff::*; -use language::language_settings::FormatOnSave; -pub use mention::*; -use project::lsp_store::{FormatTrigger, LspFormatTarget}; -use serde::{Deserialize, Serialize}; -pub use terminal::*; +pub use old_acp_support::*; -use action_log::ActionLog; use agent_client_protocol as acp; -use anyhow::{Context as _, Result, anyhow}; -use editor::Bias; +use anyhow::{Context as _, Result}; +use assistant_tool::ActionLog; +use buffer_diff::BufferDiff; +use editor::{Bias, MultiBuffer, PathKey}; use futures::{FutureExt, channel::oneshot, future::BoxFuture}; -use gpui::{AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity}; +use gpui::{AppContext, Context, Entity, EventEmitter, SharedString, Task}; use itertools::Itertools; -use language::{Anchor, Buffer, BufferSnapshot, LanguageRegistry, Point, ToPoint, text_diff}; +use language::{ + Anchor, Buffer, BufferSnapshot, Capability, LanguageRegistry, OffsetRangeExt as _, Point, + text_diff, +}; use markdown::Markdown; -use project::{AgentLocation, Project, git_store::GitStoreCheckpoint}; +use project::{AgentLocation, Project}; use std::collections::HashMap; use std::error::Error; -use std::fmt::{Formatter, Write}; -use std::ops::Range; -use std::process::ExitStatus; +use std::fmt::Formatter; use std::rc::Rc; -use std::time::{Duration, Instant}; -use std::{fmt::Display, mem, path::PathBuf, sync::Arc}; +use std::{ + fmt::Display, + mem, + path::{Path, PathBuf}, + sync::Arc, +}; use ui::App; use util::ResultExt; #[derive(Debug)] pub struct UserMessage { - pub id: Option, pub content: ContentBlock, - pub chunks: Vec, - pub checkpoint: Option, -} - -#[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 self - .checkpoint - .as_ref() - .is_some_and(|checkpoint| checkpoint.show) - { - writeln!(markdown, "## User (checkpoint)").unwrap(); - } else { - writeln!(markdown, "## User").unwrap(); + pub fn from_acp( + message: impl IntoIterator, + language_registry: Arc, + cx: &mut App, + ) -> Self { + let mut content = ContentBlock::Empty; + for chunk in message { + content.append(chunk, &language_registry, cx) } - writeln!(markdown).unwrap(); - writeln!(markdown, "{}", self.content.to_markdown(cx)).unwrap(); - writeln!(markdown).unwrap(); - markdown + Self { content: content } + } + + fn to_markdown(&self, cx: &App) -> String { + format!("## User\n\n{}\n\n", self.content.to_markdown(cx)) + } +} + +#[derive(Debug)] +pub struct MentionPath<'a>(&'a Path); + +impl<'a> MentionPath<'a> { + const PREFIX: &'static str = "@file:"; + + pub fn new(path: &'a Path) -> Self { + MentionPath(path) + } + + pub fn try_parse(url: &'a str) -> Option { + let path = url.strip_prefix(Self::PREFIX)?; + Some(MentionPath(Path::new(path))) + } + + pub fn path(&self) -> &Path { + self.0 + } +} + +impl Display for MentionPath<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "[@{}]({}{})", + self.0.file_name().unwrap_or_default().display(), + Self::PREFIX, + self.0.display() + ) } } @@ -114,7 +133,7 @@ pub enum AgentThreadEntry { } impl AgentThreadEntry { - pub fn to_markdown(&self, cx: &App) -> String { + fn to_markdown(&self, cx: &App) -> String { match self { Self::UserMessage(message) => message.to_markdown(cx), Self::AssistantMessage(message) => message.to_markdown(cx), @@ -122,15 +141,7 @@ 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> { + pub fn diffs(&self) -> impl Iterator { if let AgentThreadEntry::ToolCall(call) = self { itertools::Either::Left(call.diffs()) } else { @@ -138,25 +149,9 @@ impl AgentThreadEntry { } } - pub fn terminals(&self) -> impl Iterator> { - if let AgentThreadEntry::ToolCall(call) = self { - itertools::Either::Left(call.terminals()) - } else { - itertools::Either::Right(std::iter::empty()) - } - } - - pub fn location(&self, ix: usize) -> Option<(acp::ToolCallLocation, AgentLocation)> { - if let AgentThreadEntry::ToolCall(ToolCall { - locations, - resolved_locations, - .. - }) = self - { - Some(( - locations.get(ix)?.clone(), - resolved_locations.get(ix)?.clone()?, - )) + pub fn locations(&self) -> Option<&[acp::ToolCallLocation]> { + if let AgentThreadEntry::ToolCall(ToolCall { locations, .. }) = self { + Some(locations) } else { None } @@ -171,9 +166,7 @@ pub struct ToolCall { pub content: Vec, pub status: ToolCallStatus, pub locations: Vec, - pub resolved_locations: Vec>, pub raw_input: Option, - pub raw_output: Option, } impl ToolCall { @@ -183,15 +176,16 @@ impl ToolCall { language_registry: Arc, cx: &mut App, ) -> Self { - let title = if let Some((first_line, _)) = tool_call.title.split_once("\n") { - first_line.to_owned() + "…" - } else { - tool_call.title - }; Self { id: tool_call.id, - label: cx - .new(|cx| Markdown::new(title.into(), Some(language_registry.clone()), None, cx)), + label: cx.new(|cx| { + Markdown::new( + tool_call.label.into(), + Some(language_registry.clone()), + None, + cx, + ) + }), kind: tool_call.kind, content: tool_call .content @@ -199,14 +193,12 @@ impl ToolCall { .map(|content| ToolCallContent::from_acp(content, language_registry.clone(), cx)) .collect(), locations: tool_call.locations, - resolved_locations: Vec::default(), status, raw_input: tool_call.raw_input, - raw_output: tool_call.raw_output, } } - fn update_fields( + fn update( &mut self, fields: acp::ToolCallUpdateFields, language_registry: Arc, @@ -215,11 +207,10 @@ impl ToolCall { let acp::ToolCallUpdateFields { kind, status, - title, + label, content, locations, raw_input, - raw_output, } = fields; if let Some(kind) = kind { @@ -227,35 +218,18 @@ impl ToolCall { } if let Some(status) = status { - self.status = status.into(); + self.status = ToolCallStatus::Allowed { status }; } - if let Some(title) = title { - self.label.update(cx, |label, 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(label) = label { + self.label = cx.new(|cx| Markdown::new_text(label.into(), cx)); } if let Some(content) = content { - 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(), cx); - } - for new in content { - self.content.push(ToolCallContent::from_acp( - new, - language_registry.clone(), - cx, - )) - } - self.content.truncate(new_content_len); + self.content = content + .into_iter() + .map(|chunk| ToolCallContent::from_acp(chunk, language_registry.clone(), cx)) + .collect(); } if let Some(locations) = locations { @@ -265,33 +239,12 @@ impl ToolCall { if let Some(raw_input) = raw_input { self.raw_input = Some(raw_input); } - - if let Some(raw_output) = raw_output { - 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); - } } - pub fn diffs(&self) -> impl Iterator> { + pub fn diffs(&self) -> impl Iterator { self.content.iter().filter_map(|content| match content { - ToolCallContent::Diff(diff) => Some(diff), - ToolCallContent::ContentBlock(_) => None, - ToolCallContent::Terminal(_) => None, - }) - } - - pub fn terminals(&self) -> impl Iterator> { - self.content.iter().filter_map(|content| match content { - ToolCallContent::Terminal(terminal) => Some(terminal), - ToolCallContent::ContentBlock(_) => None, - ToolCallContent::Diff(_) => None, + ToolCallContent::ContentBlock { .. } => None, + ToolCallContent::Diff { diff } => Some(diff), }) } @@ -307,101 +260,34 @@ impl ToolCall { } markdown } - - async fn resolve_location( - location: acp::ToolCallLocation, - project: WeakEntity, - cx: &mut AsyncApp, - ) -> Option { - let buffer = project - .update(cx, |project, cx| { - project - .project_path_for_absolute_path(&location.path, cx) - .map(|path| project.open_buffer(path, cx)) - }) - .ok()??; - let buffer = buffer.await.log_err()?; - let position = buffer - .update(cx, |buffer, _| { - if let Some(row) = location.line { - let snapshot = buffer.snapshot(); - let column = snapshot.indent_size_for_line(row).len; - let point = snapshot.clip_point(Point::new(row, column), Bias::Left); - snapshot.anchor_before(point) - } else { - Anchor::MIN - } - }) - .ok()?; - - Some(AgentLocation { - buffer: buffer.downgrade(), - position, - }) - } - - fn resolve_locations( - &self, - project: Entity, - cx: &mut App, - ) -> Task>> { - let locations = self.locations.clone(); - project.update(cx, |_, cx| { - cx.spawn(async move |project, cx| { - let mut new_locations = Vec::new(); - for location in locations { - new_locations.push(Self::resolve_location(location, project.clone(), cx).await); - } - new_locations - }) - }) - } } #[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, respond_tx: oneshot::Sender, }, - /// The tool call is currently running. - InProgress, - /// The tool call completed successfully. - Completed, - /// The tool call failed. - Failed, - /// The user rejected the tool call. + Allowed { + status: acp::ToolCallStatus, + }, Rejected, - /// The user canceled generation so the tool call was canceled. Canceled, } -impl From 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::InProgress => "In Progress", - ToolCallStatus::Completed => "Completed", - ToolCallStatus::Failed => "Failed", + ToolCallStatus::Allowed { status } => match status { + acp::ToolCallStatus::Pending => "Pending", + acp::ToolCallStatus::InProgress => "In Progress", + acp::ToolCallStatus::Completed => "Completed", + acp::ToolCallStatus::Failed => "Failed", + }, ToolCallStatus::Rejected => "Rejected", ToolCallStatus::Canceled => "Canceled", } @@ -413,7 +299,6 @@ impl Display for ToolCallStatus { pub enum ContentBlock { Empty, Markdown { markdown: Entity }, - ResourceLink { resource_link: acp::ResourceLink }, } impl ContentBlock { @@ -445,78 +330,43 @@ impl ContentBlock { language_registry: &Arc, cx: &mut App, ) { - 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); + let new_content = match block { + acp::ContentBlock::Text(text_content) => text_content.text.clone(), + acp::ContentBlock::ResourceLink(resource_link) => { + if let Some(path) = resource_link.uri.strip_prefix("file://") { + format!("{}", MentionPath(path.as_ref())) + } else { + resource_link.uri.clone() + } + } + acp::ContentBlock::Image(_) + | acp::ContentBlock::Audio(_) + | acp::ContentBlock::Resource(_) => String::new(), + }; match self { ContentBlock::Empty => { - *self = Self::create_markdown_block(new_content, language_registry, cx); + *self = ContentBlock::Markdown { + markdown: cx.new(|cx| { + Markdown::new( + new_content.into(), + Some(language_registry.clone()), + None, + cx, + ) + }), + }; } ContentBlock::Markdown { markdown } => { markdown.update(cx, |markdown, cx| markdown.append(&new_content, cx)); } - ContentBlock::ResourceLink { resource_link } => { - let existing_content = Self::resource_link_md(&resource_link.uri); - let combined = format!("{}\n{}", existing_content, new_content); - - *self = Self::create_markdown_block(combined, language_registry, cx); - } } } - fn create_markdown_block( - content: String, - language_registry: &Arc, - cx: &mut App, - ) -> ContentBlock { - ContentBlock::Markdown { - markdown: cx - .new(|cx| Markdown::new(content.into(), Some(language_registry.clone()), None, cx)), - } - } - - fn block_string_contents(&self, block: acp::ContentBlock) -> String { - match block { - acp::ContentBlock::Text(text_content) => text_content.text, - acp::ContentBlock::ResourceLink(resource_link) => { - Self::resource_link_md(&resource_link.uri) - } - acp::ContentBlock::Resource(acp::EmbeddedResource { - resource: - acp::EmbeddedResourceResource::TextResourceContents(acp::TextResourceContents { - uri, - .. - }), - .. - }) => Self::resource_link_md(&uri), - 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() { - uri.as_link().to_string() - } else { - uri.to_string() - } - } - - fn image_md(_image: &acp::ImageContent) -> String { - "`Image`".into() - } - - pub fn to_markdown<'a>(&'a self, cx: &'a App) -> &'a str { + fn to_markdown<'a>(&'a self, cx: &'a App) -> &'a str { match self { ContentBlock::Empty => "", ContentBlock::Markdown { markdown } => markdown.read(cx).source(), - ContentBlock::ResourceLink { resource_link } => &resource_link.uri, } } @@ -524,23 +374,14 @@ impl ContentBlock { match self { ContentBlock::Empty => None, ContentBlock::Markdown { markdown } => Some(markdown), - ContentBlock::ResourceLink { .. } => None, - } - } - - pub fn resource_link(&self) -> Option<&acp::ResourceLink> { - match self { - ContentBlock::ResourceLink { resource_link } => Some(resource_link), - _ => None, } } } #[derive(Debug)] pub enum ToolCallContent { - ContentBlock(ContentBlock), - Diff(Entity), - Terminal(Entity), + ContentBlock { content: ContentBlock }, + Diff { diff: Diff }, } impl ToolCallContent { @@ -550,99 +391,121 @@ impl ToolCallContent { cx: &mut App, ) -> 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::finalized( - diff.path, - diff.old_text, - diff.new_text, - language_registry, - cx, - ) - })), - } - } - - pub fn update_from_acp( - &mut self, - new: acp::ToolCallContent, - language_registry: Arc, - cx: &mut App, - ) { - 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, cx); + acp::ToolCallContent::ContentBlock(content) => Self::ContentBlock { + content: ContentBlock::new(content, &language_registry, cx), + }, + acp::ToolCallContent::Diff { diff } => Self::Diff { + diff: Diff::from_acp(diff, language_registry, cx), + }, } } pub fn to_markdown(&self, cx: &App) -> String { match self { - Self::ContentBlock(content) => content.to_markdown(cx).to_string(), - Self::Diff(diff) => diff.read(cx).to_markdown(cx), - Self::Terminal(terminal) => terminal.read(cx).to_markdown(cx), + Self::ContentBlock { content } => content.to_markdown(cx).to_string(), + Self::Diff { diff } => diff.to_markdown(cx), } } } -#[derive(Debug, PartialEq)] -pub enum ToolCallUpdate { - UpdateFields(acp::ToolCallUpdate), - UpdateDiff(ToolCallUpdateDiff), - UpdateTerminal(ToolCallUpdateTerminal), +#[derive(Debug)] +pub struct Diff { + pub multibuffer: Entity, + pub path: PathBuf, + pub new_buffer: Entity, + pub old_buffer: Entity, + _task: Task>, } -impl ToolCallUpdate { - fn id(&self) -> &acp::ToolCallId { - match self { - Self::UpdateFields(update) => &update.id, - Self::UpdateDiff(diff) => &diff.id, - Self::UpdateTerminal(terminal) => &terminal.id, +impl Diff { + pub fn from_acp( + diff: acp::Diff, + language_registry: Arc, + cx: &mut App, + ) -> 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 old_buffer_snapshot = old_buffer.read(cx).snapshot(); + let buffer_diff = cx.new(|cx| BufferDiff::new(&new_buffer_snapshot, cx)); + let diff_task = buffer_diff.update(cx, |diff, cx| { + diff.set_base_text( + old_buffer_snapshot, + Some(language_registry.clone()), + new_buffer_snapshot, + cx, + ) + }); + + let task = cx.spawn({ + let multibuffer = multibuffer.clone(); + let path = path.clone(); + let new_buffer = new_buffer.clone(); + async move |cx| { + diff_task.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)) + .collect::>() + }; + + multibuffer.set_excerpts_for_path( + PathKey::for_buffer(&new_buffer, cx), + new_buffer.clone(), + hunk_ranges, + editor::DEFAULT_MULTIBUFFER_CONTEXT, + cx, + ); + multibuffer.add_diff(buffer_diff.clone(), cx); + }) + .log_err(); + + if let Some(language) = language_registry + .language_for_file_path(&path) + .await + .log_err() + { + new_buffer.update(cx, |buffer, cx| buffer.set_language(Some(language), cx))?; + } + + anyhow::Ok(()) + } + }); + + Self { + multibuffer, + path, + new_buffer, + old_buffer, + _task: task, } } -} -impl From for ToolCallUpdate { - fn from(update: acp::ToolCallUpdate) -> Self { - Self::UpdateFields(update) + fn to_markdown(&self, cx: &App) -> String { + let buffer_text = self + .multibuffer + .read(cx) + .all_buffers() + .iter() + .map(|buffer| buffer.read(cx).text()) + .join("\n"); + format!("Diff: {}\n```\n{}\n```\n", self.path.display(), buffer_text) } } -impl From for ToolCallUpdate { - fn from(diff: ToolCallUpdateDiff) -> Self { - Self::UpdateDiff(diff) - } -} - -#[derive(Debug, PartialEq)] -pub struct ToolCallUpdateDiff { - pub id: acp::ToolCallId, - pub diff: Entity, -} - -impl From for ToolCallUpdate { - fn from(terminal: ToolCallUpdateTerminal) -> Self { - Self::UpdateTerminal(terminal) - } -} - -#[derive(Debug, PartialEq)] -pub struct ToolCallUpdateTerminal { - pub id: acp::ToolCallId, - pub terminal: Entity, -} - #[derive(Debug, Default)] pub struct Plan { pub entries: Vec, @@ -695,59 +558,13 @@ pub struct PlanEntry { impl PlanEntry { pub fn from_acp(entry: acp::PlanEntry, cx: &mut App) -> Self { Self { - content: cx.new(|cx| Markdown::new(entry.content.into(), None, None, cx)), + content: cx.new(|cx| Markdown::new_text(entry.content.into(), cx)), priority: entry.priority, status: entry.status, } } } -#[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, @@ -758,29 +575,16 @@ pub struct AcpThread { send_task: Option>, connection: Rc, session_id: acp::SessionId, - token_usage: Option, - prompt_capabilities: acp::PromptCapabilities, - _observe_prompt_capabilities: Task>, } -#[derive(Debug)] pub enum AcpThreadEvent { NewEntry, - TitleUpdated, - TokenUsageUpdated, EntryUpdated(usize), - EntriesRemoved(Range), - ToolAuthorizationRequired, - Retry(RetryStatus), - Stopped, - Error, - LoadError(LoadError), - PromptCapabilitiesUpdated, } impl EventEmitter for AcpThread {} -#[derive(PartialEq, Eq, Debug)] +#[derive(PartialEq, Eq)] pub enum ThreadStatus { Idle, WaitingForToolConfirmation, @@ -789,30 +593,20 @@ pub enum ThreadStatus { #[derive(Debug, Clone)] pub enum LoadError { - NotInstalled { - error_message: SharedString, - install_message: SharedString, - install_command: String, - }, Unsupported { error_message: SharedString, upgrade_message: SharedString, upgrade_command: String, }, - Exited { - status: ExitStatus, - }, + Exited(i32), Other(SharedString), } impl Display for LoadError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - LoadError::NotInstalled { error_message, .. } - | LoadError::Unsupported { error_message, .. } => { - write!(f, "{error_message}") - } - LoadError::Exited { status } => write!(f, "Server exited with status {status}"), + LoadError::Unsupported { error_message, .. } => write!(f, "{}", error_message), + LoadError::Exited(status) => write!(f, "Server exited with status {}", status), LoadError::Other(msg) => write!(f, "{}", msg), } } @@ -822,49 +616,26 @@ impl Error for LoadError {} impl AcpThread { pub fn new( - title: impl Into, connection: Rc, project: Entity, - action_log: Entity, session_id: acp::SessionId, - mut prompt_capabilities_rx: watch::Receiver, cx: &mut Context, ) -> Self { - 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 action_log = cx.new(|_| ActionLog::new(project.clone())); Self { action_log, shared_buffers: Default::default(), entries: Default::default(), plan: Default::default(), - title: title.into(), + title: connection.name().into(), project, send_task: None, connection, session_id, - token_usage: None, - prompt_capabilities, - _observe_prompt_capabilities: task, } } - pub fn prompt_capabilities(&self) -> acp::PromptCapabilities { - self.prompt_capabilities - } - - pub fn connection(&self) -> &Rc { - &self.connection - } - pub fn action_log(&self) -> &Entity { &self.action_log } @@ -881,10 +652,6 @@ impl AcpThread { &self.entries } - pub fn session_id(&self) -> &acp::SessionId { - &self.session_id - } - pub fn status(&self) -> ThreadStatus { if self.send_task.is_some() { if self.waiting_for_tool_confirmation() { @@ -897,22 +664,11 @@ impl AcpThread { } } - 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::InProgress | ToolCallStatus::Pending, - .. - }, - ) if call.diffs().next().is_some() => { - return true; - } + AgentThreadEntry::ToolCall(call) if call.diffs().next().is_some() => return true, AgentThreadEntry::ToolCall(_) | AgentThreadEntry::AssistantMessage(_) => {} } } @@ -920,35 +676,23 @@ impl AcpThread { false } - pub fn used_tools_since_last_user_message(&self) -> bool { - for entry in self.entries.iter().rev() { - match entry { - AgentThreadEntry::UserMessage(..) => return false, - AgentThreadEntry::AssistantMessage(..) => continue, - AgentThreadEntry::ToolCall(..) => return true, - } - } - - false - } - pub fn handle_session_update( &mut self, update: acp::SessionUpdate, cx: &mut Context, - ) -> Result<(), acp::Error> { + ) -> Result<()> { match update { - acp::SessionUpdate::UserMessageChunk { content } => { - self.push_user_content_block(None, content, cx); + acp::SessionUpdate::UserMessage(content_block) => { + self.push_user_content_block(content_block, cx); } - acp::SessionUpdate::AgentMessageChunk { content } => { - self.push_assistant_content_block(content, false, cx); + acp::SessionUpdate::AgentMessageChunk(content_block) => { + self.push_assistant_content_block(content_block, false, cx); } - acp::SessionUpdate::AgentThoughtChunk { content } => { - self.push_assistant_content_block(content, true, cx); + acp::SessionUpdate::AgentThoughtChunk(content_block) => { + self.push_assistant_content_block(content_block, 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)?; @@ -960,39 +704,18 @@ impl AcpThread { Ok(()) } - pub fn push_user_content_block( - &mut self, - message_id: Option, - chunk: acp::ContentBlock, - cx: &mut Context, - ) { + pub fn push_user_content_block(&mut self, chunk: acp::ContentBlock, cx: &mut Context) { let language_registry = self.project.read(cx).languages().clone(); let entries_len = self.entries.len(); if let Some(last_entry) = self.entries.last_mut() - && let AgentThreadEntry::UserMessage(UserMessage { - id, - content, - chunks, - .. - }) = last_entry + && let AgentThreadEntry::UserMessage(UserMessage { content }) = last_entry { - *id = message_id.or(id.take()); - content.append(chunk.clone(), &language_registry, cx); - chunks.push(chunk); - let idx = entries_len - 1; - cx.emit(AcpThreadEvent::EntryUpdated(idx)); + content.append(chunk, &language_registry, cx); + cx.emit(AcpThreadEvent::EntryUpdated(entries_len - 1)); } else { - 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, - ); + let content = ContentBlock::new(chunk, &language_registry, cx); + self.push_entry(AgentThreadEntry::UserMessage(UserMessage { content }), cx); } } @@ -1007,8 +730,7 @@ impl AcpThread { if let Some(last_entry) = self.entries.last_mut() && let AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) = last_entry { - let idx = entries_len - 1; - cx.emit(AcpThreadEvent::EntryUpdated(idx)); + cx.emit(AcpThreadEvent::EntryUpdated(entries_len - 1)); match (chunks.last_mut(), is_thought) { (Some(AssistantMessageChunk::Message { block }), false) | (Some(AssistantMessageChunk::Thought { block }), true) => { @@ -1045,62 +767,17 @@ impl AcpThread { cx.emit(AcpThreadEvent::NewEntry); } - pub fn can_set_title(&mut self, cx: &mut Context) -> bool { - self.connection.set_title(&self.session_id, cx).is_some() - } - - pub fn set_title(&mut self, title: SharedString, cx: &mut Context) -> Task> { - 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, cx: &mut Context) { - self.token_usage = usage; - cx.emit(AcpThreadEvent::TokenUsageUpdated); - } - - pub fn update_retry_status(&mut self, status: RetryStatus, cx: &mut Context) { - cx.emit(AcpThreadEvent::Retry(status)); - } - pub fn update_tool_call( &mut self, - update: impl Into, + update: acp::ToolCallUpdate, cx: &mut Context, ) -> Result<()> { - let update = update.into(); let languages = self.project.read(cx).languages().clone(); let (ix, current_call) = self - .tool_call_mut(update.id()) + .tool_call_mut(&update.id) .context("Tool call not found")?; - match update { - ToolCallUpdate::UpdateFields(update) => { - let location_updated = update.fields.locations.is_some(); - current_call.update_fields(update.fields, languages, cx); - if location_updated { - self.resolve_locations(update.id, cx); - } - } - ToolCallUpdate::UpdateDiff(update) => { - current_call.content.clear(); - current_call - .content - .push(ToolCallContent::Diff(update.diff)); - } - ToolCallUpdate::UpdateTerminal(update) => { - current_call.content.clear(); - current_call - .content - .push(ToolCallContent::Terminal(update.terminal)); - } - } + current_call.update(update.fields, languages, cx); cx.emit(AcpThreadEvent::EntryUpdated(ix)); @@ -1108,38 +785,35 @@ 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, - ) -> Result<(), acp::Error> { - let status = tool_call.status.into(); - self.upsert_tool_call_inner(tool_call.into(), status, cx) + pub fn upsert_tool_call(&mut self, tool_call: acp::ToolCall, cx: &mut Context) { + let status = ToolCallStatus::Allowed { + status: tool_call.status, + }; + self.upsert_tool_call_inner(tool_call, status, cx) } - /// Fails if id does not match an existing entry. pub fn upsert_tool_call_inner( &mut self, - tool_call_update: acp::ToolCallUpdate, + tool_call: acp::ToolCall, status: ToolCallStatus, cx: &mut Context, - ) -> Result<(), acp::Error> { + ) { let language_registry = self.project.read(cx).languages().clone(); - let id = tool_call_update.id.clone(); + let call = ToolCall::from_acp(tool_call, status, language_registry, cx); - if let Some((ix, current_call)) = self.tool_call_mut(&id) { - current_call.update_fields(tool_call_update.fields, language_registry, cx); - current_call.status = status; + let location = call.locations.last().cloned(); + + if let Some((ix, current_call)) = self.tool_call_mut(&call.id) { + *current_call = call; cx.emit(AcpThreadEvent::EntryUpdated(ix)); } else { - let call = - ToolCall::from_acp(tool_call_update.try_into()?, status, language_registry, cx); self.push_entry(AgentThreadEntry::ToolCall(call), cx); - }; + } - self.resolve_locations(id, cx); - Ok(()) + if let Some(location) = location { + self.set_project_location(location, cx) + } } fn tool_call_mut(&mut self, id: &acp::ToolCallId) -> Option<(usize, &mut ToolCall)> { @@ -1160,74 +834,43 @@ 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 set_project_location(&self, location: acp::ToolCallLocation, cx: &mut Context) { + self.project.update(cx, |project, cx| { + let Some(path) = project.project_path_for_absolute_path(&location.path, cx) else { + return; + }; + let buffer = project.open_buffer(path, cx); + cx.spawn(async move |project, cx| { + let buffer = buffer.await?; + + project.update(cx, |project, cx| { + let position = if let Some(line) = location.line { + let snapshot = buffer.read(cx).snapshot(); + let point = snapshot.clip_point(Point::new(line, 0), Bias::Left); + snapshot.anchor_before(point) + } else { + Anchor::MIN + }; + + project.set_agent_location( + Some(AgentLocation { + buffer: buffer.downgrade(), + position, + }), + cx, + ); + }) }) + .detach_and_log_err(cx); + }); } - pub fn resolve_locations(&mut self, id: acp::ToolCallId, cx: &mut Context) { - let project = self.project.clone(); - let Some((_, tool_call)) = self.tool_call_mut(&id) else { - return; - }; - let task = tool_call.resolve_locations(project, cx); - cx.spawn(async move |this, cx| { - let resolved_locations = task.await; - this.update(cx, |this, cx| { - let project = this.project.clone(); - let Some((ix, tool_call)) = this.tool_call_mut(&id) else { - return; - }; - if let Some(Some(location)) = resolved_locations.last() { - project.update(cx, |project, cx| { - if let Some(agent_location) = project.agent_location() { - let should_ignore = agent_location.buffer == location.buffer - && location - .buffer - .update(cx, |buffer, _| { - let snapshot = buffer.snapshot(); - let old_position = - agent_location.position.to_point(&snapshot); - let new_position = location.position.to_point(&snapshot); - // ignore this so that when we get updates from the edit tool - // the position doesn't reset to the startof line - old_position.row == new_position.row - && old_position.column > new_position.column - }) - .ok() - .unwrap_or_default(); - if !should_ignore { - project.set_agent_location(Some(location.clone()), cx); - } - } - }); - } - if tool_call.resolved_locations != resolved_locations { - tool_call.resolved_locations = resolved_locations; - cx.emit(AcpThreadEvent::EntryUpdated(ix)); - } - }) - }) - .detach(); - } - - pub fn request_tool_call_authorization( + pub fn request_tool_call_permission( &mut self, - tool_call: acp::ToolCallUpdate, + tool_call: acp::ToolCall, options: Vec, cx: &mut Context, - ) -> Result, acp::Error> { + ) -> oneshot::Receiver { let (tx, rx) = oneshot::channel(); let status = ToolCallStatus::WaitingForConfirmation { @@ -1235,9 +878,8 @@ impl AcpThread { respond_tx: tx, }; - self.upsert_tool_call_inner(tool_call, status, cx)?; - cx.emit(AcpThreadEvent::ToolAuthorizationRequired); - Ok(rx) + self.upsert_tool_call_inner(tool_call, status, cx); + rx } pub fn authorize_tool_call( @@ -1256,7 +898,9 @@ impl AcpThread { ToolCallStatus::Rejected } acp::PermissionOptionKind::AllowOnce | acp::PermissionOptionKind::AllowAlways => { - ToolCallStatus::InProgress + ToolCallStatus::Allowed { + status: acp::ToolCallStatus::InProgress, + } } }; @@ -1277,10 +921,7 @@ impl AcpThread { match &entry { AgentThreadEntry::ToolCall(call) => match call.status { ToolCallStatus::WaitingForConfirmation { .. } => return true, - ToolCallStatus::Pending - | ToolCallStatus::InProgress - | ToolCallStatus::Completed - | ToolCallStatus::Failed + ToolCallStatus::Allowed { .. } | ToolCallStatus::Rejected | ToolCallStatus::Canceled => continue, }, @@ -1298,26 +939,13 @@ impl AcpThread { } pub fn update_plan(&mut self, request: acp::Plan, cx: &mut Context) { - let new_entries_len = request.entries.len(); - let mut new_entries = request.entries.into_iter(); - - // Reuse existing markdown to prevent flickering - for (old, new) in self.plan.entries.iter_mut().zip(new_entries.by_ref()) { - let PlanEntry { - content, - priority, - status, - } = old; - content.update(cx, |old, cx| { - old.replace(new.content, cx); - }); - *priority = new.priority; - *status = new.status; - } - for new in new_entries { - self.plan.entries.push(PlanEntry::from_acp(new, cx)) - } - self.plan.entries.truncate(new_entries_len); + self.plan = Plan { + entries: request + .entries + .into_iter() + .map(|entry| PlanEntry::from_acp(entry, cx)) + .collect(), + }; cx.notify(); } @@ -1329,6 +957,10 @@ impl AcpThread { cx.notify(); } + pub fn authenticate(&self, cx: &mut App) -> impl use<> + Future> { + self.connection.authenticate(cx) + } + #[cfg(any(test, feature = "test-support"))] pub fn send_raw( &mut self, @@ -1354,130 +986,44 @@ 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 message_id = if self.connection.truncate(&self.session_id, cx).is_some() { - Some(UserMessageId::new()) - } else { - None - }; - - 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) -> 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, - f: impl 'static + AsyncFnOnce(WeakEntity, &mut AsyncApp) -> Result, - ) -> BoxFuture<'static, Result<()>> { + self.push_entry( + AgentThreadEntry::UserMessage(UserMessage { content: block }), + cx, + ); self.clear_completed_plan_entries(cx); let (tx, rx) = oneshot::channel(); let cancel_task = self.cancel(cx); self.send_task = Some(cx.spawn(async move |this, cx| { - cancel_task.await; - tx.send(f(this, cx).await).ok(); + async { + cancel_task.await; + + let result = this + .update(cx, |this, cx| { + this.connection.prompt( + acp::PromptArguments { + prompt: message, + session_id: this.session_id.clone(), + }, + cx, + ) + })? + .await; + tx.send(result).log_err(); + this.update(cx, |this, _cx| this.send_task.take())?; + anyhow::Ok(()) + } + .await + .log_err(); })); - cx.spawn(async move |this, cx| { - let response = rx.await; - - 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(); - cx.emit(AcpThreadEvent::Error); - Err(e) - } - result => { - let canceled = matches!( - result, - Ok(Ok(acp::PromptResponse { - stop_reason: acp::StopReason::Cancelled - })) - ); - - // We only take the task if the current prompt wasn't canceled. - // - // This prompt may have been canceled because another one was sent - // while it was still generating. In these cases, dropping `send_task` - // would cause the next generation to be canceled. - if !canceled { - this.send_task.take(); - } - - // Truncate entries if the last prompt was refused. - if let Ok(Ok(acp::PromptResponse { - stop_reason: acp::StopReason::Refusal, - })) = result - && let Some((ix, _)) = this.last_user_message() - { - let range = ix..this.entries.len(); - this.entries.truncate(ix); - cx.emit(AcpThreadEvent::EntriesRemoved(range)); - } - - cx.emit(AcpThreadEvent::Stopped); - Ok(()) - } - } - })? - }) + async move { + match rx.await { + Ok(Err(e)) => Err(e)?, + _ => Ok(()), + } + } .boxed() } @@ -1490,9 +1036,10 @@ impl AcpThread { if let AgentThreadEntry::ToolCall(call) = entry { let cancel = matches!( call.status, - ToolCallStatus::Pending - | ToolCallStatus::WaitingForConfirmation { .. } - | ToolCallStatus::InProgress + ToolCallStatus::WaitingForConfirmation { .. } + | ToolCallStatus::Allowed { + status: acp::ToolCallStatus::InProgress + } ); if cancel { @@ -1507,121 +1054,6 @@ impl AcpThread { cx.foreground_executor().spawn(send_task) } - /// Rewinds this thread to before the entry at `index`, removing it and all - /// subsequent entries while reverting any changes made from that point. - pub fn rewind(&mut self, id: UserMessageId, cx: &mut Context) -> Task> { - let Some(truncate) = self.connection.truncate(&self.session_id, cx) else { - return Task::ready(Err(anyhow!("not supported"))); - }; - let Some(message) = self.user_message(&id) else { - return Task::ready(Err(anyhow!("message not found"))); - }; - - let checkpoint = message - .checkpoint - .as_ref() - .map(|c| c.git_checkpoint.clone()); - - let git_store = self.project.read(cx).git_store().clone(); - cx.spawn(async move |this, cx| { - if let Some(checkpoint) = checkpoint { - git_store - .update(cx, |git, cx| git.restore_checkpoint(checkpoint, cx))? - .await?; - } - - cx.update(|cx| truncate.run(id.clone(), cx))?.await?; - this.update(cx, |this, cx| { - if let Some((ix, _)) = this.user_message_mut(&id) { - let range = ix..this.entries.len(); - this.entries.truncate(ix); - cx.emit(AcpThreadEvent::EntriesRemoved(range)); - } - }) - }) - } - - fn update_last_checkpoint(&mut self, cx: &mut Context) -> Task> { - let git_store = self.project.read(cx).git_store().clone(); - - let old_checkpoint = if let Some((_, message)) = self.last_user_message() { - if let Some(checkpoint) = message.checkpoint.as_ref() { - checkpoint.git_checkpoint.clone() - } else { - return Task::ready(Ok(())); - } - } else { - return Task::ready(Ok(())); - }; - - let new_checkpoint = git_store.update(cx, |git, cx| git.checkpoint(cx)); - cx.spawn(async move |this, cx| { - let new_checkpoint = new_checkpoint - .await - .context("failed to get new checkpoint") - .log_err(); - if let Some(new_checkpoint) = new_checkpoint { - let equal = git_store - .update(cx, |git, cx| { - git.compare_checkpoints(old_checkpoint.clone(), new_checkpoint, cx) - })? - .await - .unwrap_or(true); - this.update(cx, |this, cx| { - let (ix, message) = this.last_user_message().context("no user message")?; - let checkpoint = message.checkpoint.as_mut().context("no checkpoint")?; - checkpoint.show = !equal; - cx.emit(AcpThreadEvent::EntryUpdated(ix)); - anyhow::Ok(()) - })??; - } - - Ok(()) - }) - } - - fn last_user_message(&mut self) -> Option<(usize, &mut UserMessage)> { - self.entries - .iter_mut() - .enumerate() - .rev() - .find_map(|(ix, entry)| { - if let AgentThreadEntry::UserMessage(message) = entry { - Some((ix, message)) - } else { - None - } - }) - } - - fn user_message(&self, id: &UserMessageId) -> Option<&UserMessage> { - self.entries.iter().find_map(|entry| { - if let AgentThreadEntry::UserMessage(message) = entry { - if message.id.as_ref() == Some(id) { - Some(message) - } else { - None - } - } else { - None - } - }) - } - - fn user_message_mut(&mut self, id: &UserMessageId) -> Option<(usize, &mut UserMessage)> { - self.entries.iter_mut().enumerate().find_map(|(ix, entry)| { - if let AgentThreadEntry::UserMessage(message) = entry { - if message.id.as_ref() == Some(id) { - Some((ix, message)) - } else { - None - } - } else { - None - } - }) - } - pub fn read_text_file( &self, path: PathBuf, @@ -1736,59 +1168,30 @@ impl AcpThread { .collect::>() }) .await; + cx.update(|cx| { + project.update(cx, |project, cx| { + project.set_agent_location( + Some(AgentLocation { + buffer: buffer.downgrade(), + position: edits + .last() + .map(|(range, _)| range.end) + .unwrap_or(Anchor::MIN), + }), + cx, + ); + }); - project.update(cx, |project, cx| { - project.set_agent_location( - Some(AgentLocation { - buffer: buffer.downgrade(), - position: edits - .last() - .map(|(range, _)| range.end) - .unwrap_or(Anchor::MIN), - }), - cx, - ); - })?; - - let format_on_save = cx.update(|cx| { action_log.update(cx, |action_log, cx| { action_log.buffer_read(buffer.clone(), cx); }); - - let format_on_save = buffer.update(cx, |buffer, cx| { + buffer.update(cx, |buffer, cx| { buffer.edit(edits, None, cx); - - let settings = language::language_settings::language_settings( - buffer.language().map(|l| l.name()), - buffer.file(), - cx, - ); - - settings.format_on_save != FormatOnSave::Off }); action_log.update(cx, |action_log, cx| { action_log.buffer_edited(buffer.clone(), cx); }); - format_on_save })?; - - if format_on_save { - let format_task = project.update(cx, |project, cx| { - project.format( - HashSet::from_iter([buffer.clone()]), - LspFormatTarget::Buffers, - false, - FormatTrigger::Save, - cx, - ) - })?; - format_task.await.log_err(); - - action_log.update(cx, |action_log, cx| { - action_log.buffer_edited(buffer.clone(), cx); - })?; - } - project .update(cx, |project, cx| project.save_buffer(buffer, cx))? .await @@ -1798,74 +1201,23 @@ impl AcpThread { pub fn to_markdown(&self, cx: &App) -> String { self.entries.iter().map(|e| e.to_markdown(cx)).collect() } - - pub fn emit_load_error(&mut self, error: LoadError, cx: &mut Context) { - cx.emit(AcpThreadEvent::LoadError(error)); - } -} - -fn markdown_for_raw_output( - raw_output: &serde_json::Value, - language_registry: &Arc, - cx: &mut App, -) -> Option> { - match raw_output { - serde_json::Value::Null => None, - serde_json::Value::Bool(value) => Some(cx.new(|cx| { - Markdown::new( - value.to_string().into(), - Some(language_registry.clone()), - None, - cx, - ) - })), - serde_json::Value::Number(value) => Some(cx.new(|cx| { - Markdown::new( - value.to_string().into(), - Some(language_registry.clone()), - None, - cx, - ) - })), - serde_json::Value::String(value) => Some(cx.new(|cx| { - Markdown::new( - value.clone().into(), - Some(language_registry.clone()), - None, - cx, - ) - })), - value => Some(cx.new(|cx| { - Markdown::new( - format!("```json\n{}\n```", value).into(), - Some(language_registry.clone()), - None, - cx, - ) - })), - } } #[cfg(test)] mod tests { use super::*; + use agentic_coding_protocol as acp_old; use anyhow::anyhow; + use async_pipe::{PipeReader, PipeWriter}; use futures::{channel::mpsc, future::LocalBoxFuture, select}; - use gpui::{App, AsyncApp, TestAppContext, WeakEntity}; + use gpui::{AsyncApp, TestAppContext}; use indoc::indoc; - use project::{FakeFs, Fs}; - use rand::Rng as _; + use project::FakeFs; use serde_json::json; use settings::SettingsStore; - use smol::stream::StreamExt as _; - use std::{ - any::Any, - cell::RefCell, - path::Path, - rc::Rc, - sync::atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst}, - time::Duration, - }; + use smol::{future::BoxedLocal, stream::StreamExt as _}; + use std::{cell::RefCell, rc::Rc, time::Duration}; + use util::path; fn init_test(cx: &mut TestAppContext) { @@ -1884,16 +1236,11 @@ mod tests { let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, [], cx).await; - let connection = Rc::new(FakeAgentConnection::new()); - let thread = cx - .update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx)) - .await - .unwrap(); + let (thread, _fake_server) = fake_acp_thread(project, cx); // Test creating a new user message thread.update(cx, |thread, cx| { thread.push_user_content_block( - None, acp::ContentBlock::Text(acp::TextContent { annotations: None, text: "Hello, ".to_string(), @@ -1905,7 +1252,6 @@ mod tests { thread.update(cx, |thread, cx| { assert_eq!(thread.entries.len(), 1); if let AgentThreadEntry::UserMessage(user_msg) = &thread.entries[0] { - assert_eq!(user_msg.id, None); assert_eq!(user_msg.content.to_markdown(cx), "Hello, "); } else { panic!("Expected UserMessage"); @@ -1913,10 +1259,8 @@ mod tests { }); // Test appending to existing user message - let message_1_id = UserMessageId::new(); thread.update(cx, |thread, cx| { thread.push_user_content_block( - Some(message_1_id.clone()), acp::ContentBlock::Text(acp::TextContent { annotations: None, text: "world!".to_string(), @@ -1928,7 +1272,6 @@ mod tests { thread.update(cx, |thread, cx| { assert_eq!(thread.entries.len(), 1); if let AgentThreadEntry::UserMessage(user_msg) = &thread.entries[0] { - assert_eq!(user_msg.id, Some(message_1_id)); assert_eq!(user_msg.content.to_markdown(cx), "Hello, world!"); } else { panic!("Expected UserMessage"); @@ -1947,10 +1290,8 @@ mod tests { ); }); - let message_2_id = UserMessageId::new(); thread.update(cx, |thread, cx| { thread.push_user_content_block( - Some(message_2_id.clone()), acp::ContentBlock::Text(acp::TextContent { annotations: None, text: "New user message".to_string(), @@ -1962,7 +1303,6 @@ mod tests { thread.update(cx, |thread, cx| { assert_eq!(thread.entries.len(), 3); if let AgentThreadEntry::UserMessage(user_msg) = &thread.entries[2] { - assert_eq!(user_msg.id, Some(message_2_id)); assert_eq!(user_msg.content.to_markdown(cx), "New user message"); } else { panic!("Expected UserMessage at index 2"); @@ -1976,39 +1316,34 @@ mod tests { let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, [], cx).await; - let connection = Rc::new(FakeAgentConnection::new().on_user_message( - |_, thread, mut cx| { - async move { - thread.update(&mut cx, |thread, cx| { - thread - .handle_session_update( - acp::SessionUpdate::AgentThoughtChunk { - content: "Thinking ".into(), - }, - cx, - ) - .unwrap(); - thread - .handle_session_update( - acp::SessionUpdate::AgentThoughtChunk { - content: "hard!".into(), - }, - cx, - ) - .unwrap(); - })?; - Ok(acp::PromptResponse { - stop_reason: acp::StopReason::EndTurn, - }) - } - .boxed_local() - }, - )); + let (thread, fake_server) = fake_acp_thread(project, cx); - let thread = cx - .update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx)) - .await - .unwrap(); + fake_server.update(cx, |fake_server, _| { + fake_server.on_user_message(move |_, server, mut cx| async move { + server + .update(&mut cx, |server, _| { + server.send_to_zed(acp_old::StreamAssistantMessageChunkParams { + chunk: acp_old::AssistantMessageChunk::Thought { + thought: "Thinking ".into(), + }, + }) + })? + .await + .unwrap(); + server + .update(&mut cx, |server, _| { + server.send_to_zed(acp_old::StreamAssistantMessageChunkParams { + chunk: acp_old::AssistantMessageChunk::Thought { + thought: "hard!".into(), + }, + }) + })? + .await + .unwrap(); + + Ok(()) + }) + }); thread .update(cx, |thread, cx| thread.send_raw("Hello from Zed!", cx)) @@ -2041,40 +1376,7 @@ mod tests { fs.insert_tree(path!("/tmp"), json!({"foo": "one\ntwo\nthree\n"})) .await; let project = Project::test(fs.clone(), [], cx).await; - let (read_file_tx, read_file_rx) = oneshot::channel::<()>(); - let read_file_tx = Rc::new(RefCell::new(Some(read_file_tx))); - let connection = Rc::new(FakeAgentConnection::new().on_user_message( - move |_, thread, mut cx| { - let read_file_tx = read_file_tx.clone(); - async move { - let content = thread - .update(&mut cx, |thread, cx| { - thread.read_text_file(path!("/tmp/foo").into(), None, None, false, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!(content, "one\ntwo\nthree\n"); - read_file_tx.take().unwrap().send(()).unwrap(); - thread - .update(&mut cx, |thread, cx| { - thread.write_text_file( - path!("/tmp/foo").into(), - "one\ntwo\nthree\nfour\nfive\n".to_string(), - cx, - ) - }) - .unwrap() - .await - .unwrap(); - Ok(acp::PromptResponse { - stop_reason: acp::StopReason::EndTurn, - }) - } - .boxed_local() - }, - )); - + let (thread, fake_server) = fake_acp_thread(project.clone(), cx); let (worktree, pathbuf) = project .update(cx, |project, cx| { project.find_or_create_worktree(path!("/tmp/foo"), true, cx) @@ -2088,10 +1390,38 @@ mod tests { .await .unwrap(); - let thread = cx - .update(|cx| connection.new_thread(project, Path::new(path!("/tmp")), cx)) - .await - .unwrap(); + let (read_file_tx, read_file_rx) = oneshot::channel::<()>(); + let read_file_tx = Rc::new(RefCell::new(Some(read_file_tx))); + + fake_server.update(cx, |fake_server, _| { + fake_server.on_user_message(move |_, server, mut cx| { + let read_file_tx = read_file_tx.clone(); + async move { + let content = server + .update(&mut cx, |server, _| { + server.send_to_zed(acp_old::ReadTextFileParams { + path: path!("/tmp/foo").into(), + line: None, + limit: None, + }) + })? + .await + .unwrap(); + assert_eq!(content.content, "one\ntwo\nthree\n"); + read_file_tx.take().unwrap().send(()).unwrap(); + server + .update(&mut cx, |server, _| { + server.send_to_zed(acp_old::WriteTextFileParams { + path: path!("/tmp/foo").into(), + content: "one\ntwo\nthree\nfour\nfive\n".to_string(), + }) + })? + .await + .unwrap(); + Ok(()) + } + }) + }); let request = thread.update(cx, |thread, cx| { thread.send_raw("Extend the count in /tmp/foo", cx) @@ -2118,43 +1448,36 @@ mod tests { let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, [], cx).await; - let id = acp::ToolCallId("test".into()); + let (thread, fake_server) = fake_acp_thread(project, cx); - let connection = Rc::new(FakeAgentConnection::new().on_user_message({ - let id = id.clone(); - move |_, thread, mut cx| { - let id = id.clone(); + let (end_turn_tx, end_turn_rx) = oneshot::channel::<()>(); + + let tool_call_id = Rc::new(RefCell::new(None)); + let end_turn_rx = Rc::new(RefCell::new(Some(end_turn_rx))); + fake_server.update(cx, |fake_server, _| { + let tool_call_id = tool_call_id.clone(); + fake_server.on_user_message(move |_, server, mut cx| { + let end_turn_rx = end_turn_rx.clone(); + let tool_call_id = tool_call_id.clone(); async move { - thread - .update(&mut cx, |thread, cx| { - thread.handle_session_update( - acp::SessionUpdate::ToolCall(acp::ToolCall { - id: id.clone(), - title: "Label".into(), - kind: acp::ToolKind::Fetch, - status: acp::ToolCallStatus::InProgress, - content: vec![], - locations: vec![], - raw_input: None, - raw_output: None, - }), - cx, - ) - }) - .unwrap() + let tool_call_result = server + .update(&mut cx, |server, _| { + server.send_to_zed(acp_old::PushToolCallParams { + label: "Fetch".to_string(), + icon: acp_old::Icon::Globe, + content: None, + locations: vec![], + }) + })? + .await .unwrap(); - Ok(acp::PromptResponse { - stop_reason: acp::StopReason::EndTurn, - }) - } - .boxed_local() - } - })); + *tool_call_id.clone().borrow_mut() = Some(tool_call_result.id); + end_turn_rx.take().unwrap().await.ok(); - let thread = cx - .update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx)) - .await - .unwrap(); + Ok(()) + } + }) + }); let request = thread.update(cx, |thread, cx| { thread.send_raw("Fetch https://example.com", cx) @@ -2166,12 +1489,17 @@ mod tests { assert!(matches!( thread.entries[1], AgentThreadEntry::ToolCall(ToolCall { - status: ToolCallStatus::InProgress, + status: ToolCallStatus::Allowed { + status: acp::ToolCallStatus::InProgress, + .. + }, .. }) )); }); + cx.run_until_parked(); + thread.update(cx, |thread, cx| thread.cancel(cx)).await; thread.read_with(cx, |thread, _| { @@ -2184,354 +1512,34 @@ mod tests { )); }); - thread - .update(cx, |thread, cx| { - thread.handle_session_update( - acp::SessionUpdate::ToolCallUpdate(acp::ToolCallUpdate { - id, - fields: acp::ToolCallUpdateFields { - status: Some(acp::ToolCallStatus::Completed), - ..Default::default() - }, - }), - cx, - ) + fake_server + .update(cx, |fake_server, _| { + fake_server.send_to_zed(acp_old::UpdateToolCallParams { + tool_call_id: tool_call_id.borrow().unwrap(), + status: acp_old::ToolCallStatus::Finished, + content: None, + }) }) + .await .unwrap(); - request.await.unwrap(); + drop(end_turn_tx); + assert!(request.await.unwrap_err().to_string().contains("canceled")); thread.read_with(cx, |thread, _| { assert!(matches!( thread.entries[1], AgentThreadEntry::ToolCall(ToolCall { - status: ToolCallStatus::Completed, + status: ToolCallStatus::Allowed { + status: acp::ToolCallStatus::Completed, + .. + }, .. }) )); }); } - #[gpui::test] - async fn test_no_pending_edits_if_tool_calls_are_completed(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree(path!("/test"), json!({})).await; - let project = Project::test(fs, [path!("/test").as_ref()], cx).await; - - let connection = Rc::new(FakeAgentConnection::new().on_user_message({ - move |_, thread, mut cx| { - async move { - thread - .update(&mut cx, |thread, cx| { - thread.handle_session_update( - acp::SessionUpdate::ToolCall(acp::ToolCall { - id: acp::ToolCallId("test".into()), - title: "Label".into(), - kind: acp::ToolKind::Edit, - status: acp::ToolCallStatus::Completed, - content: vec![acp::ToolCallContent::Diff { - diff: acp::Diff { - path: "/test/test.txt".into(), - old_text: None, - new_text: "foo".into(), - }, - }], - locations: vec![], - raw_input: None, - raw_output: None, - }), - cx, - ) - }) - .unwrap() - .unwrap(); - Ok(acp::PromptResponse { - stop_reason: acp::StopReason::EndTurn, - }) - } - .boxed_local() - } - })); - - let thread = cx - .update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx)) - .await - .unwrap(); - - cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["Hi".into()], cx))) - .await - .unwrap(); - - assert!(cx.read(|cx| !thread.read(cx).has_pending_edit_tool_calls())); - } - - #[gpui::test(iterations = 10)] - async fn test_checkpoints(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/test"), - json!({ - ".git": {} - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await; - - let simulate_changes = Arc::new(AtomicBool::new(true)); - let next_filename = Arc::new(AtomicUsize::new(0)); - let connection = Rc::new(FakeAgentConnection::new().on_user_message({ - let simulate_changes = simulate_changes.clone(); - let next_filename = next_filename.clone(); - let fs = fs.clone(); - move |request, thread, mut cx| { - let fs = fs.clone(); - let simulate_changes = simulate_changes.clone(); - let next_filename = next_filename.clone(); - async move { - if simulate_changes.load(SeqCst) { - let filename = format!("/test/file-{}", next_filename.fetch_add(1, SeqCst)); - fs.write(Path::new(&filename), b"").await?; - } - - let acp::ContentBlock::Text(content) = &request.prompt[0] else { - panic!("expected text content block"); - }; - thread.update(&mut cx, |thread, cx| { - thread - .handle_session_update( - acp::SessionUpdate::AgentMessageChunk { - content: content.text.to_uppercase().into(), - }, - cx, - ) - .unwrap(); - })?; - Ok(acp::PromptResponse { - stop_reason: acp::StopReason::EndTurn, - }) - } - .boxed_local() - } - })); - let thread = cx - .update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx)) - .await - .unwrap(); - - cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["Lorem".into()], cx))) - .await - .unwrap(); - thread.read_with(cx, |thread, cx| { - assert_eq!( - thread.to_markdown(cx), - indoc! {" - ## User (checkpoint) - - Lorem - - ## Assistant - - LOREM - - "} - ); - }); - assert_eq!(fs.files(), vec![Path::new(path!("/test/file-0"))]); - - cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["ipsum".into()], cx))) - .await - .unwrap(); - thread.read_with(cx, |thread, cx| { - assert_eq!( - thread.to_markdown(cx), - indoc! {" - ## User (checkpoint) - - Lorem - - ## Assistant - - LOREM - - ## User (checkpoint) - - ipsum - - ## Assistant - - IPSUM - - "} - ); - }); - assert_eq!( - fs.files(), - vec![ - Path::new(path!("/test/file-0")), - Path::new(path!("/test/file-1")) - ] - ); - - // Checkpoint isn't stored when there are no changes. - simulate_changes.store(false, SeqCst); - cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["dolor".into()], cx))) - .await - .unwrap(); - thread.read_with(cx, |thread, cx| { - assert_eq!( - thread.to_markdown(cx), - indoc! {" - ## User (checkpoint) - - Lorem - - ## Assistant - - LOREM - - ## User (checkpoint) - - ipsum - - ## Assistant - - IPSUM - - ## User - - dolor - - ## Assistant - - DOLOR - - "} - ); - }); - assert_eq!( - fs.files(), - vec![ - Path::new(path!("/test/file-0")), - Path::new(path!("/test/file-1")) - ] - ); - - // Rewinding the conversation truncates the history and restores the checkpoint. - thread - .update(cx, |thread, cx| { - let AgentThreadEntry::UserMessage(message) = &thread.entries[2] else { - panic!("unexpected entries {:?}", thread.entries) - }; - thread.rewind(message.id.clone().unwrap(), cx) - }) - .await - .unwrap(); - thread.read_with(cx, |thread, cx| { - assert_eq!( - thread.to_markdown(cx), - indoc! {" - ## User (checkpoint) - - Lorem - - ## Assistant - - LOREM - - "} - ); - }); - assert_eq!(fs.files(), vec![Path::new(path!("/test/file-0"))]); - } - - #[gpui::test] - async fn test_refusal(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree(path!("/"), json!({})).await; - let project = Project::test(fs.clone(), [path!("/").as_ref()], cx).await; - - let refuse_next = Arc::new(AtomicBool::new(false)); - let connection = Rc::new(FakeAgentConnection::new().on_user_message({ - let refuse_next = refuse_next.clone(); - move |request, thread, mut cx| { - let refuse_next = refuse_next.clone(); - async move { - if refuse_next.load(SeqCst) { - return Ok(acp::PromptResponse { - stop_reason: acp::StopReason::Refusal, - }); - } - - let acp::ContentBlock::Text(content) = &request.prompt[0] else { - panic!("expected text content block"); - }; - thread.update(&mut cx, |thread, cx| { - thread - .handle_session_update( - acp::SessionUpdate::AgentMessageChunk { - content: content.text.to_uppercase().into(), - }, - cx, - ) - .unwrap(); - })?; - Ok(acp::PromptResponse { - stop_reason: acp::StopReason::EndTurn, - }) - } - .boxed_local() - } - })); - let thread = cx - .update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx)) - .await - .unwrap(); - - cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["hello".into()], cx))) - .await - .unwrap(); - thread.read_with(cx, |thread, cx| { - assert_eq!( - thread.to_markdown(cx), - indoc! {" - ## User - - hello - - ## Assistant - - HELLO - - "} - ); - }); - - // Simulate refusing the second message, ensuring the conversation gets - // truncated to before sending it. - refuse_next.store(true, SeqCst); - cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["world".into()], cx))) - .await - .unwrap(); - thread.read_with(cx, |thread, cx| { - assert_eq!( - thread.to_markdown(cx), - indoc! {" - ## User - - hello - - ## Assistant - - HELLO - - "} - ); - }); - } - async fn run_until_first_tool_call( thread: &Entity, cx: &mut TestAppContext, @@ -2559,152 +1567,169 @@ mod tests { } } - #[derive(Clone, Default)] - struct FakeAgentConnection { - auth_methods: Vec, - sessions: Arc>>>, + pub fn fake_acp_thread( + project: Entity, + cx: &mut TestAppContext, + ) -> (Entity, Entity) { + let (stdin_tx, stdin_rx) = async_pipe::pipe(); + let (stdout_tx, stdout_rx) = async_pipe::pipe(); + + let thread = cx.new(|cx| { + let foreground_executor = cx.foreground_executor().clone(); + let thread_rc = Rc::new(RefCell::new(cx.entity().downgrade())); + + let (connection, io_fut) = acp_old::AgentConnection::connect_to_agent( + OldAcpClientDelegate::new(thread_rc.clone(), cx.to_async()), + stdin_tx, + stdout_rx, + move |fut| { + foreground_executor.spawn(fut).detach(); + }, + ); + + let io_task = cx.background_spawn({ + async move { + io_fut.await.log_err(); + Ok(()) + } + }); + let connection = OldAcpAgentConnection { + name: "test", + connection, + child_status: io_task, + current_thread: thread_rc, + }; + + AcpThread::new( + Rc::new(connection), + project, + acp::SessionId("test".into()), + cx, + ) + }); + let agent = cx.update(|cx| cx.new(|cx| FakeAcpServer::new(stdin_rx, stdout_tx, cx))); + (thread, agent) + } + + pub struct FakeAcpServer { + connection: acp_old::ClientConnection, + + _io_task: Task<()>, on_user_message: Option< Rc< dyn Fn( - acp::PromptRequest, - WeakEntity, - AsyncApp, - ) -> LocalBoxFuture<'static, Result> - + 'static, + acp_old::SendUserMessageParams, + Entity, + AsyncApp, + ) -> LocalBoxFuture<'static, Result<(), acp_old::Error>>, >, >, } - impl FakeAgentConnection { - fn new() -> Self { - Self { - auth_methods: Vec::new(), - on_user_message: None, - sessions: Arc::default(), - } - } - - #[expect(unused)] - fn with_auth_methods(mut self, auth_methods: Vec) -> Self { - self.auth_methods = auth_methods; - self - } - - fn on_user_message( - mut self, - handler: impl Fn( - acp::PromptRequest, - WeakEntity, - AsyncApp, - ) -> LocalBoxFuture<'static, Result> - + 'static, - ) -> Self { - self.on_user_message.replace(Rc::new(handler)); - self - } + #[derive(Clone)] + struct FakeAgent { + server: Entity, + cx: AsyncApp, + cancel_tx: Rc>>>, } - impl AgentConnection for FakeAgentConnection { - fn auth_methods(&self) -> &[acp::AuthMethod] { - &self.auth_methods - } - - fn new_thread( - self: Rc, - project: Entity, - _cwd: &Path, - cx: &mut App, - ) -> Task>> { - let session_id = acp::SessionId( - rand::thread_rng() - .sample_iter(&rand::distributions::Alphanumeric) - .take(7) - .map(char::from) - .collect::() - .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, thread.downgrade()); - Task::ready(Ok(thread)) - } - - fn authenticate(&self, method: acp::AuthMethodId, _cx: &mut App) -> Task> { - if self.auth_methods().iter().any(|m| m.id == method) { - Task::ready(Ok(())) - } else { - Task::ready(Err(anyhow!("Invalid Auth Method"))) - } - } - - fn prompt( + impl acp_old::Agent for FakeAgent { + async fn initialize( &self, - _id: Option, - params: acp::PromptRequest, - cx: &mut App, - ) -> Task> { - let sessions = self.sessions.lock(); - let thread = sessions.get(¶ms.session_id).unwrap(); - if let Some(handler) = &self.on_user_message { - let handler = handler.clone(); - let thread = thread.clone(); - cx.spawn(async move |cx| handler(params, thread, cx.clone()).await) - } else { - Task::ready(Ok(acp::PromptResponse { - stop_reason: acp::StopReason::EndTurn, - })) - } - } - - fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { - let sessions = self.sessions.lock(); - let thread = sessions.get(session_id).unwrap().clone(); - - cx.spawn(async move |cx| { - thread - .update(cx, |thread, cx| thread.cancel(cx)) - .unwrap() - .await + params: acp_old::InitializeParams, + ) -> Result { + Ok(acp_old::InitializeResponse { + protocol_version: params.protocol_version, + is_authenticated: true, }) - .detach(); } - fn truncate( + async fn authenticate(&self) -> Result<(), acp_old::Error> { + Ok(()) + } + + async fn cancel_send_message(&self) -> Result<(), acp_old::Error> { + if let Some(cancel_tx) = self.cancel_tx.take() { + cancel_tx.send(()).log_err(); + } + Ok(()) + } + + async fn send_user_message( &self, - session_id: &acp::SessionId, - _cx: &App, - ) -> Option> { - Some(Rc::new(FakeAgentSessionEditor { - _session_id: session_id.clone(), - })) - } + request: acp_old::SendUserMessageParams, + ) -> Result<(), acp_old::Error> { + let (cancel_tx, cancel_rx) = oneshot::channel(); + self.cancel_tx.replace(Some(cancel_tx)); - fn into_any(self: Rc) -> Rc { - self + let mut cx = self.cx.clone(); + let handler = self + .server + .update(&mut cx, |server, _| server.on_user_message.clone()) + .ok() + .flatten(); + if let Some(handler) = handler { + select! { + _ = cancel_rx.fuse() => Err(anyhow::anyhow!("Message sending canceled").into()), + _ = handler(request, self.server.clone(), self.cx.clone()).fuse() => Ok(()), + } + } else { + Err(anyhow::anyhow!("No handler for on_user_message").into()) + } } } - struct FakeAgentSessionEditor { - _session_id: acp::SessionId, - } + impl FakeAcpServer { + fn new(stdin: PipeReader, stdout: PipeWriter, cx: &Context) -> Self { + let agent = FakeAgent { + server: cx.entity(), + cx: cx.to_async(), + cancel_tx: Default::default(), + }; + let foreground_executor = cx.foreground_executor().clone(); - impl AgentSessionTruncate for FakeAgentSessionEditor { - fn run(&self, _message_id: UserMessageId, _cx: &mut App) -> Task> { - Task::ready(Ok(())) + let (connection, io_fut) = acp_old::ClientConnection::connect_to_client( + agent.clone(), + stdout, + stdin, + move |fut| { + foreground_executor.spawn(fut).detach(); + }, + ); + FakeAcpServer { + connection: connection, + on_user_message: None, + _io_task: cx.background_spawn(async move { + io_fut.await.log_err(); + }), + } + } + + fn on_user_message( + &mut self, + handler: impl for<'a> Fn( + acp_old::SendUserMessageParams, + Entity, + AsyncApp, + ) -> F + + 'static, + ) where + F: Future> + 'static, + { + self.on_user_message + .replace(Rc::new(move |request, server, cx| { + handler(request, server, cx).boxed_local() + })); + } + + fn send_to_zed( + &self, + message: T, + ) -> BoxedLocal> { + self.connection + .request(message) + .map(|f| f.map_err(|err| anyhow!(err))) + .boxed_local() } } } diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index af229b7545..5b25b71863 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -1,458 +1,26 @@ -use crate::AcpThread; -use agent_client_protocol::{self as acp}; +use std::{path::Path, rc::Rc}; + +use agent_client_protocol as acp; use anyhow::Result; -use collections::IndexMap; -use gpui::{Entity, SharedString, Task}; -use language_model::LanguageModelProviderId; +use gpui::{AsyncApp, Entity, Task}; use project::Project; -use serde::{Deserialize, Serialize}; -use std::{any::Any, error::Error, fmt, path::Path, rc::Rc, sync::Arc}; -use ui::{App, IconName}; -use uuid::Uuid; +use ui::App; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] -pub struct UserMessageId(Arc); - -impl UserMessageId { - pub fn new() -> Self { - Self(Uuid::new_v4().to_string().into()) - } -} +use crate::AcpThread; pub trait AgentConnection { + fn name(&self) -> &'static str; + fn new_thread( self: Rc, project: Entity, cwd: &Path, - cx: &mut App, + cx: &mut AsyncApp, ) -> Task>>; - fn auth_methods(&self) -> &[acp::AuthMethod]; + fn authenticate(&self, cx: &mut App) -> Task>; - fn authenticate(&self, method: acp::AuthMethodId, cx: &mut App) -> Task>; - - fn prompt( - &self, - user_message_id: Option, - params: acp::PromptRequest, - cx: &mut App, - ) -> Task>; - - fn resume( - &self, - _session_id: &acp::SessionId, - _cx: &App, - ) -> Option> { - None - } + fn prompt(&self, params: acp::PromptArguments, cx: &mut App) -> Task>; fn cancel(&self, session_id: &acp::SessionId, cx: &mut App); - - fn truncate( - &self, - _session_id: &acp::SessionId, - _cx: &App, - ) -> Option> { - None - } - - fn set_title( - &self, - _session_id: &acp::SessionId, - _cx: &App, - ) -> Option> { - None - } - - /// Returns this agent as an [Rc] if the model selection capability is supported. - /// - /// If the agent does not support model selection, returns [None]. - /// This allows sharing the selector in UI components. - fn model_selector(&self) -> Option> { - None - } - - fn telemetry(&self) -> Option> { - None - } - - fn into_any(self: Rc) -> Rc; } - -impl dyn AgentConnection { - pub fn downcast(self: Rc) -> Option> { - self.into_any().downcast().ok() - } -} - -pub trait AgentSessionTruncate { - fn run(&self, message_id: UserMessageId, cx: &mut App) -> Task>; -} - -pub trait AgentSessionResume { - fn run(&self, cx: &mut App) -> Task>; -} - -pub trait AgentSessionSetTitle { - fn run(&self, title: SharedString, cx: &mut App) -> Task>; -} - -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>; -} - -#[derive(Debug)] -pub struct AuthRequired { - pub description: Option, - pub provider_id: Option, -} - -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, "Authentication required") - } -} - -/// Trait for agents that support listing, selecting, and querying language models. -/// -/// This is an optional capability; agents indicate support via [AgentConnection::model_selector]. -pub trait AgentModelSelector: 'static { - /// Lists all available language models for this agent. - /// - /// # Parameters - /// - `cx`: The GPUI app context for async operations and global access. - /// - /// # Returns - /// A task resolving to the list of models or an error (e.g., if no models are configured). - fn list_models(&self, cx: &mut App) -> Task>; - - /// Selects a model for a specific session (thread). - /// - /// This sets the default model for future interactions in the session. - /// If the session doesn't exist or the model is invalid, it returns an error. - /// - /// # Parameters - /// - `session_id`: The ID of the session (thread) to apply the model to. - /// - `model`: The model to select (should be one from [list_models]). - /// - `cx`: The GPUI app context. - /// - /// # Returns - /// A task resolving to `Ok(())` on success or an error. - fn select_model( - &self, - session_id: acp::SessionId, - model_id: AgentModelId, - cx: &mut App, - ) -> Task>; - - /// Retrieves the currently selected model for a specific session (thread). - /// - /// # Parameters - /// - `session_id`: The ID of the session (thread) to query. - /// - `cx`: The GPUI app context. - /// - /// # Returns - /// A task resolving to the selected model (always set) or an error (e.g., session not found). - fn selected_model( - &self, - session_id: &acp::SessionId, - cx: &mut App, - ) -> Task>; - - /// Whenever the model list is updated the receiver will be notified. - fn watch(&self, cx: &mut App) -> watch::Receiver<()>; -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct AgentModelId(pub SharedString); - -impl std::ops::Deref for AgentModelId { - type Target = SharedString; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl fmt::Display for AgentModelId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct AgentModelInfo { - pub id: AgentModelId, - pub name: SharedString, - pub icon: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct AgentModelGroupName(pub SharedString); - -#[derive(Debug, Clone)] -pub enum AgentModelList { - Flat(Vec), - Grouped(IndexMap>), -} - -impl AgentModelList { - pub fn is_empty(&self) -> bool { - match self { - AgentModelList::Flat(models) => models.is_empty(), - AgentModelList::Grouped(groups) => groups.is_empty(), - } - } -} - -#[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>>, - permission_requests: HashMap>, - next_prompt_updates: Arc>>, - } - - struct Session { - thread: WeakEntity, - response_tx: Option>, - } - - 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) { - *self.next_prompt_updates.lock() = updates; - } - - pub fn with_permission_requests( - mut self, - permission_requests: HashMap>, - ) -> 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, - project: Entity, - _cwd: &Path, - cx: &mut gpui::App, - ) -> Task>> { - 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> { - unimplemented!() - } - - fn prompt( - &self, - _id: Option, - params: acp::PromptRequest, - cx: &mut App, - ) -> Task> { - 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 { - let permission = thread.update(cx, |thread, cx| { - thread.request_tool_call_authorization( - tool_call.clone().into(), - options.clone(), - cx, - ) - })?; - permission?.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> { - Some(Rc::new(StubAgentSessionEditor)) - } - - fn into_any(self: Rc) -> Rc { - self - } - } - - struct StubAgentSessionEditor; - - impl AgentSessionTruncate for StubAgentSessionEditor { - fn run(&self, _: UserMessageId, _: &mut App) -> Task> { - Task::ready(Ok(())) - } - } -} - -#[cfg(feature = "test-support")] -pub use test_support::*; diff --git a/crates/acp_thread/src/diff.rs b/crates/acp_thread/src/diff.rs deleted file mode 100644 index 0fec6809e0..0000000000 --- a/crates/acp_thread/src/diff.rs +++ /dev/null @@ -1,424 +0,0 @@ -use anyhow::Result; -use buffer_diff::{BufferDiff, BufferDiffSnapshot}; -use editor::{MultiBuffer, PathKey}; -use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task}; -use itertools::Itertools; -use language::{ - Anchor, Buffer, Capability, LanguageRegistry, OffsetRangeExt as _, Point, Rope, TextBuffer, -}; -use std::{ - cmp::Reverse, - ops::Range, - path::{Path, PathBuf}, - sync::Arc, -}; -use util::ResultExt; - -pub enum Diff { - Pending(PendingDiff), - Finalized(FinalizedDiff), -} - -impl Diff { - pub fn finalized( - path: PathBuf, - old_text: Option, - new_text: String, - language_registry: Arc, - cx: &mut Context, - ) -> Self { - let multibuffer = cx.new(|_cx| MultiBuffer::without_headers(Capability::ReadOnly)); - let new_buffer = cx.new(|cx| Buffer::local(new_text, 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(); - - buffer.update(cx, |buffer, cx| buffer.set_language(language.clone(), cx))?; - - 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 = 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::>() - }; - - multibuffer.set_excerpts_for_path( - PathKey::for_buffer(&buffer, cx), - buffer.clone(), - hunk_ranges, - editor::DEFAULT_MULTIBUFFER_CONTEXT, - cx, - ); - multibuffer.add_diff(diff, cx); - }) - .log_err(); - - anyhow::Ok(()) - } - }); - - Self::Finalized(FinalizedDiff { - multibuffer, - path, - base_text, - new_buffer, - _update_diff: task, - }) - } - - pub fn new(buffer: Entity, cx: &mut Context) -> Self { - let buffer_text_snapshot = buffer.read(cx).text_snapshot(); - let base_text_snapshot = buffer.read(cx).snapshot(); - let base_text = base_text_snapshot.text(); - debug_assert_eq!(buffer_text_snapshot.text(), base_text); - let buffer_diff = cx.new(|cx| { - let mut diff = BufferDiff::new_unchanged(&buffer_text_snapshot, base_text_snapshot); - let snapshot = diff.snapshot(cx); - let secondary_diff = cx.new(|cx| { - let mut diff = BufferDiff::new(&buffer_text_snapshot, cx); - diff.set_snapshot(snapshot, &buffer_text_snapshot, cx); - diff - }); - diff.set_secondary_diff(secondary_diff); - diff - }); - - let multibuffer = cx.new(|cx| { - let mut multibuffer = MultiBuffer::without_headers(Capability::ReadOnly); - multibuffer.add_diff(buffer_diff.clone(), cx); - multibuffer - }); - - Self::Pending(PendingDiff { - multibuffer, - base_text: Arc::new(base_text), - _subscription: cx.observe(&buffer, |this, _, cx| { - if let Diff::Pending(diff) = this { - diff.update(cx); - } - }), - new_buffer: buffer, - diff: buffer_diff, - revealed_ranges: Vec::new(), - update_diff: Task::ready(Ok(())), - }) - } - - pub fn reveal_range(&mut self, range: Range, cx: &mut Context) { - if let Self::Pending(diff) = self { - diff.reveal_range(range, cx); - } - } - - pub fn finalize(&mut self, cx: &mut Context) { - if let Self::Pending(diff) = self { - *self = Self::Finalized(diff.finalize(cx)); - } - } - - pub fn multibuffer(&self) -> &Entity { - match self { - Self::Pending(PendingDiff { multibuffer, .. }) => multibuffer, - Self::Finalized(FinalizedDiff { multibuffer, .. }) => multibuffer, - } - } - - pub fn to_markdown(&self, cx: &App) -> String { - let buffer_text = self - .multibuffer() - .read(cx) - .all_buffers() - .iter() - .map(|buffer| buffer.read(cx).text()) - .join("\n"); - let path = match self { - 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!( - "Diff: {}\n```\n{}\n```\n", - path.unwrap_or(Path::new("untitled")).display(), - buffer_text - ) - } - - 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, - base_text: Arc, - new_buffer: Entity, - diff: Entity, - revealed_ranges: Vec>, - _subscription: Subscription, - update_diff: Task>, -} - -impl PendingDiff { - pub fn update(&mut self, cx: &mut Context) { - 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| { - let text_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot())?; - let diff_snapshot = BufferDiff::update_diff( - buffer_diff.clone(), - text_snapshot.clone(), - Some(base_text), - false, - false, - None, - None, - cx, - ) - .await?; - buffer_diff.update(cx, |diff, 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 { - diff.update_visible_ranges(cx); - } - }) - }); - } - - pub fn reveal_range(&mut self, range: Range, cx: &mut Context) { - self.revealed_ranges.push(range); - self.update_visible_ranges(cx); - } - - fn finalize(&self, cx: &mut Context) -> FinalizedDiff { - let ranges = self.excerpt_ranges(cx); - let base_text = self.base_text.clone(); - let language_registry = self.new_buffer.read(cx).language_registry(); - - let path = self - .new_buffer - .read(cx) - .file() - .map(|file| file.path().as_ref()) - .unwrap_or(Path::new("untitled")) - .into(); - - // Replace the buffer in the multibuffer with the snapshot - let buffer = cx.new(|cx| { - let language = self.new_buffer.read(cx).language().cloned(); - let buffer = TextBuffer::new_normalized( - 0, - cx.entity_id().as_non_zero_u64().into(), - 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); - buffer - }); - - let buffer_diff = cx.spawn({ - let buffer = buffer.clone(); - async move |_this, cx| { - build_buffer_diff(base_text, &buffer, language_registry, cx).await - } - }); - - let update_diff = cx.spawn(async move |this, cx| { - let buffer_diff = buffer_diff.await?; - this.update(cx, |this, cx| { - this.multibuffer().update(cx, |multibuffer, cx| { - let path_key = PathKey::for_buffer(&buffer, cx); - multibuffer.clear(cx); - multibuffer.set_excerpts_for_path( - path_key, - buffer, - ranges, - editor::DEFAULT_MULTIBUFFER_CONTEXT, - cx, - ); - multibuffer.add_diff(buffer_diff.clone(), cx); - }); - - cx.notify(); - }) - }); - - FinalizedDiff { - path, - base_text: self.base_text.clone(), - multibuffer: self.multibuffer.clone(), - new_buffer: self.new_buffer.clone(), - _update_diff: update_diff, - } - } - - fn update_visible_ranges(&mut self, cx: &mut Context) { - let ranges = self.excerpt_ranges(cx); - self.multibuffer.update(cx, |multibuffer, cx| { - multibuffer.set_excerpts_for_path( - PathKey::for_buffer(&self.new_buffer, cx), - self.new_buffer.clone(), - ranges, - editor::DEFAULT_MULTIBUFFER_CONTEXT, - cx, - ); - let end = multibuffer.len(cx); - Some(multibuffer.snapshot(cx).offset_to_point(end).row + 1) - }); - cx.notify(); - } - - fn excerpt_ranges(&self, cx: &App) -> Vec> { - 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)) - .collect::>(); - ranges.extend( - self.revealed_ranges - .iter() - .map(|range| range.to_point(buffer)), - ); - ranges.sort_unstable_by_key(|range| (range.start, Reverse(range.end))); - - // Merge adjacent ranges - let mut ranges = ranges.into_iter().peekable(); - let mut merged_ranges = Vec::new(); - while let Some(mut range) = ranges.next() { - while let Some(next_range) = ranges.peek() { - if range.end >= next_range.start { - range.end = range.end.max(next_range.end); - ranges.next(); - } else { - break; - } - } - - merged_ranges.push(range); - } - merged_ranges - } -} - -pub struct FinalizedDiff { - path: PathBuf, - base_text: Arc, - new_buffer: Entity, - multibuffer: Entity, - _update_diff: Task>, -} - -async fn build_buffer_diff( - old_text: Arc, - buffer: &Entity, - language_registry: Option>, - cx: &mut AsyncApp, -) -> Result> { - let buffer = cx.update(|cx| buffer.read(cx).snapshot())?; - - let old_text_rope = cx - .background_spawn({ - let old_text = old_text.clone(); - async move { Rope::from(old_text.as_str()) } - }) - .await; - let base_buffer = cx - .update(|cx| { - Buffer::build_snapshot( - old_text_rope, - buffer.language().cloned(), - language_registry, - cx, - ) - })? - .await; - - let diff_snapshot = cx - .update(|cx| { - BufferDiffSnapshot::new_with_base_buffer( - buffer.text.clone(), - Some(old_text), - base_buffer, - cx, - ) - })? - .await; - - let secondary_diff = cx.new(|cx| { - let mut diff = BufferDiff::new(&buffer, cx); - diff.set_snapshot(diff_snapshot.clone(), &buffer, cx); - diff - })?; - - cx.new(|cx| { - let mut diff = BufferDiff::new(&buffer.text, cx); - diff.set_snapshot(diff_snapshot, &buffer, cx); - diff.set_secondary_diff(secondary_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(); - } -} diff --git a/crates/acp_thread/src/mention.rs b/crates/acp_thread/src/mention.rs deleted file mode 100644 index 6fa0887e22..0000000000 --- a/crates/acp_thread/src/mention.rs +++ /dev/null @@ -1,502 +0,0 @@ -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::RangeInclusive, - path::{Path, PathBuf}, - str::FromStr, -}; -use ui::{App, IconName, SharedString}; -use url::Url; - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)] -pub enum MentionUri { - File { - abs_path: PathBuf, - }, - PastedImage, - Directory { - abs_path: PathBuf, - }, - Symbol { - abs_path: PathBuf, - name: String, - line_range: RangeInclusive, - }, - Thread { - id: acp::SessionId, - name: String, - }, - TextThread { - path: PathBuf, - name: String, - }, - Rule { - id: PromptId, - name: String, - }, - Selection { - #[serde(default, skip_serializing_if = "Option::is_none")] - abs_path: Option, - line_range: RangeInclusive, - }, - Fetch { - url: Url, - }, -} - -impl MentionUri { - pub fn parse(input: &str) -> Result { - fn parse_line_range(fragment: &str) -> Result> { - 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::() - .context("Parsing line range start")? - .checked_sub(1) - .context("Line numbers should be 1-based")? - ..=end - .parse::() - .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 line_range = parse_line_range(fragment)?; - if let Some(name) = single_query_param(&url, "symbol")? { - Ok(Self::Symbol { - name, - abs_path: path, - line_range, - }) - } else { - Ok(Self::Selection { - abs_path: Some(path), - line_range, - }) - } - } else if input.ends_with("/") { - Ok(Self::Directory { abs_path: path }) - } else { - 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: acp::SessionId(thread_id.into()), - name, - }) - } else if let Some(path) = path.strip_prefix("/agent/text-thread/") { - let name = single_query_param(&url, "name")?.context("Missing thread name")?; - Ok(Self::TextThread { - path: path.into(), - name, - }) - } else if let Some(rule_id) = path.strip_prefix("/agent/rule/") { - let name = single_query_param(&url, "name")?.context("Missing rule name")?; - let rule_id = UserPromptId(rule_id.parse()?); - Ok(Self::Rule { - 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); - } - } - "http" | "https" => Ok(MentionUri::Fetch { url }), - other => bail!("unrecognized scheme {:?}", other), - } - } - - pub fn name(&self) -> String { - match self { - 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 { - 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 { 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 { - abs_path, - name, - line_range, - } => { - 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 - ))); - url - } - 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 - ))); - url - } - MentionUri::Thread { name, id } => { - let mut url = Url::parse("zed:///").unwrap(); - url.set_path(&format!("/agent/thread/{id}")); - url.query_pairs_mut().append_pair("name", name); - url - } - MentionUri::TextThread { path, name } => { - let mut url = Url::parse("zed:///").unwrap(); - url.set_path(&format!( - "/agent/text-thread/{}", - path.to_string_lossy().trim_start_matches('/') - )); - url.query_pairs_mut().append_pair("name", name); - url - } - MentionUri::Rule { name, id } => { - let mut url = Url::parse("zed:///").unwrap(); - url.set_path(&format!("/agent/rule/{id}")); - url.query_pairs_mut().append_pair("name", name); - url - } - MentionUri::Fetch { url } => url.clone(), - } - } -} - -impl FromStr for MentionUri { - type Err = anyhow::Error; - - fn from_str(s: &str) -> anyhow::Result { - Self::parse(s) - } -} - -pub struct MentionLink<'a>(&'a MentionUri); - -impl fmt::Display for MentionLink<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "[@{}]({})", self.0.name(), self.0.to_uri()) - } -} - -fn single_query_param(url: &Url, name: &'static str) -> Result> { - let pairs = url.query_pairs().collect::>(); - match pairs.as_slice() { - [] => Ok(None), - [(k, v)] => { - if k != name { - bail!("invalid query parameter") - } - - Ok(Some(v.to_string())) - } - _ => bail!("too many query pairs"), - } -} - -pub fn selection_name(path: Option<&Path>, line_range: &RangeInclusive) -> String { - format!( - "{} ({}:{})", - 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 = uri!("file:///path/to/file.rs"); - let parsed = MentionUri::parse(file_uri).unwrap(); - match &parsed { - 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 = uri!("file:///path/to/file.rs?symbol=MySymbol#L10:20"); - let parsed = MentionUri::parse(symbol_uri).unwrap(); - match &parsed { - MentionUri::Symbol { - abs_path: path, - name, - line_range, - } => { - 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); - } - _ => panic!("Expected Symbol variant"), - } - assert_eq!(parsed.to_uri().to_string(), symbol_uri); - } - - #[test] - fn test_parse_selection_uri() { - let selection_uri = uri!("file:///path/to/file.rs#L5:15"); - let parsed = MentionUri::parse(selection_uri).unwrap(); - match &parsed { - 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"; - let parsed = MentionUri::parse(thread_uri).unwrap(); - match &parsed { - MentionUri::Thread { - id: thread_id, - name, - } => { - assert_eq!(thread_id.to_string(), "session123"); - assert_eq!(name, "Thread name"); - } - _ => panic!("Expected Thread variant"), - } - assert_eq!(parsed.to_uri().to_string(), thread_uri); - } - - #[test] - fn test_parse_rule_uri() { - let rule_uri = "zed:///agent/rule/d8694ff2-90d5-4b6f-be33-33c1763acd52?name=Some+rule"; - let parsed = MentionUri::parse(rule_uri).unwrap(); - match &parsed { - MentionUri::Rule { id, name } => { - assert_eq!(id.to_string(), "d8694ff2-90d5-4b6f-be33-33c1763acd52"); - assert_eq!(name, "Some rule"); - } - _ => panic!("Expected Rule variant"), - } - assert_eq!(parsed.to_uri().to_string(), rule_uri); - } - - #[test] - fn test_parse_fetch_http_uri() { - let http_uri = "http://example.com/path?query=value#fragment"; - let parsed = MentionUri::parse(http_uri).unwrap(); - match &parsed { - MentionUri::Fetch { url } => { - assert_eq!(url.to_string(), http_uri); - } - _ => panic!("Expected Fetch variant"), - } - assert_eq!(parsed.to_uri().to_string(), http_uri); - } - - #[test] - fn test_parse_fetch_https_uri() { - let https_uri = "https://example.com/api/endpoint"; - let parsed = MentionUri::parse(https_uri).unwrap(); - match &parsed { - MentionUri::Fetch { url } => { - assert_eq!(url.to_string(), https_uri); - } - _ => panic!("Expected Fetch variant"), - } - assert_eq!(parsed.to_uri().to_string(), https_uri); - } - - #[test] - fn test_invalid_scheme() { - assert!(MentionUri::parse("ftp://example.com").is_err()); - assert!(MentionUri::parse("ssh://example.com").is_err()); - assert!(MentionUri::parse("unknown://example.com").is_err()); - } - - #[test] - fn test_invalid_zed_path() { - assert!(MentionUri::parse("zed:///invalid/path").is_err()); - assert!(MentionUri::parse("zed:///agent/unknown/test").is_err()); - } - - #[test] - fn test_invalid_line_range_format() { - // Missing L prefix - assert!(MentionUri::parse(uri!("file:///path/to/file.rs#10:20")).is_err()); - - // Missing colon separator - assert!(MentionUri::parse(uri!("file:///path/to/file.rs#L1020")).is_err()); - - // Invalid numbers - 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(uri!("file:///path/to/file.rs#L10:20?invalid=test")).is_err()); - - // Too many query parameters - assert!( - 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(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()); - } -} diff --git a/crates/acp_thread/src/old_acp_support.rs b/crates/acp_thread/src/old_acp_support.rs new file mode 100644 index 0000000000..571023239f --- /dev/null +++ b/crates/acp_thread/src/old_acp_support.rs @@ -0,0 +1,453 @@ +// 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}; +use futures::channel::oneshot; +use gpui::{AppContext as _, AsyncApp, Entity, Task, WeakEntity}; +use project::Project; +use std::{cell::RefCell, error::Error, fmt, path::Path, rc::Rc}; +use ui::App; +use util::ResultExt as _; + +use crate::{AcpThread, AgentConnection}; + +#[derive(Clone)] +pub struct OldAcpClientDelegate { + thread: Rc>>, + cx: AsyncApp, + next_tool_call_id: Rc>, + // sent_buffer_versions: HashMap, HashMap>, +} + +impl OldAcpClientDelegate { + pub fn new(thread: Rc>>, 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 { + 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()), + label, + kind, + }) + } + + let response = cx + .update(|cx| { + self.thread.borrow().update(cx, |thread, cx| { + thread.request_tool_call_permission(tool_call, acp_options, cx) + }) + })? + .context("Failed to update thread")? + .await; + + let outcome = match response { + Ok(option_id) => outcomes[option_id.0.parse::().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 { + 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::>(), + ), + ..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 { + 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, + label: 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, + } +} + +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, + } +} + +#[derive(Debug)] +pub struct Unauthenticated; + +impl Error for Unauthenticated {} +impl fmt::Display for Unauthenticated { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Unauthenticated") + } +} + +pub struct OldAcpAgentConnection { + pub name: &'static str, + pub connection: acp_old::AgentConnection, + pub child_status: Task>, + pub current_thread: Rc>>, +} + +impl AgentConnection for OldAcpAgentConnection { + fn name(&self) -> &'static str { + self.name + } + + fn new_thread( + self: Rc, + project: Entity, + _cwd: &Path, + cx: &mut AsyncApp, + ) -> Task>> { + 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!(Unauthenticated) + } + + cx.update(|cx| { + let thread = cx.new(|cx| { + let session_id = acp::SessionId("acp-old-no-id".into()); + AcpThread::new(self.clone(), project, session_id, cx) + }); + current_thread.replace(thread.downgrade()); + thread + }) + }) + } + + fn authenticate(&self, cx: &mut App) -> Task> { + let task = self + .connection + .request_any(acp_old::AuthenticateParams.into_any()); + cx.foreground_executor().spawn(async move { + task.await?; + Ok(()) + }) + } + + fn prompt(&self, params: acp::PromptArguments, cx: &mut App) -> Task> { + 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(()) + }) + } + + 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) + } +} diff --git a/crates/acp_thread/src/terminal.rs b/crates/acp_thread/src/terminal.rs deleted file mode 100644 index 41d7fb89bb..0000000000 --- a/crates/acp_thread/src/terminal.rs +++ /dev/null @@ -1,93 +0,0 @@ -use gpui::{App, AppContext, Context, Entity}; -use language::LanguageRegistry; -use markdown::Markdown; -use std::{path::PathBuf, process::ExitStatus, sync::Arc, time::Instant}; - -pub struct Terminal { - command: Entity, - working_dir: Option, - terminal: Entity, - started_at: Instant, - output: Option, -} - -pub struct TerminalOutput { - pub ended_at: Instant, - pub exit_status: Option, - pub was_content_truncated: bool, - pub original_content_len: usize, - pub content_line_count: usize, - pub finished_with_empty_output: bool, -} - -impl Terminal { - pub fn new( - command: String, - working_dir: Option, - terminal: Entity, - language_registry: Arc, - cx: &mut Context, - ) -> Self { - Self { - command: cx.new(|cx| { - Markdown::new( - format!("```\n{}\n```", command).into(), - Some(language_registry.clone()), - None, - cx, - ) - }), - working_dir, - terminal, - started_at: Instant::now(), - output: None, - } - } - - pub fn finish( - &mut self, - exit_status: Option, - original_content_len: usize, - truncated_content_len: usize, - content_line_count: usize, - finished_with_empty_output: bool, - cx: &mut Context, - ) { - 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, - }); - cx.notify(); - } - - pub fn command(&self) -> &Entity { - &self.command - } - - pub fn working_dir(&self) -> &Option { - &self.working_dir - } - - pub fn started_at(&self) -> Instant { - self.started_at - } - - pub fn output(&self) -> Option<&TerminalOutput> { - self.output.as_ref() - } - - pub fn inner(&self) -> &Entity { - &self.terminal - } - - pub fn to_markdown(&self, cx: &App) -> String { - format!( - "Terminal:\n```\n{}\n```\n", - self.terminal.read(cx).get_content() - ) - } -} diff --git a/crates/acp_tools/Cargo.toml b/crates/acp_tools/Cargo.toml deleted file mode 100644 index 7a6d8c21a0..0000000000 --- a/crates/acp_tools/Cargo.toml +++ /dev/null @@ -1,30 +0,0 @@ -[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 diff --git a/crates/acp_tools/src/acp_tools.rs b/crates/acp_tools/src/acp_tools.rs deleted file mode 100644 index e20a040e9d..0000000000 --- a/crates/acp_tools/src/acp_tools.rs +++ /dev/null @@ -1,494 +0,0 @@ -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.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); - -impl Global for GlobalAcpConnectionRegistry {} - -#[derive(Default)] -pub struct AcpConnectionRegistry { - active_connection: RefCell>, -} - -struct ActiveConnection { - server_name: SharedString, - connection: Weak, -} - -impl AcpConnectionRegistry { - pub fn default_global(cx: &mut App) -> Entity { - if cx.has_global::() { - cx.global::().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, - connection: &Rc, - cx: &mut Context, - ) { - self.active_connection.replace(Some(ActiveConnection { - server_name: server_name.into(), - connection: Rc::downgrade(connection), - })); - cx.notify(); - } -} - -struct AcpTools { - project: Entity, - focus_handle: FocusHandle, - expanded: HashSet, - watched_connection: Option, - connection_registry: Entity, - _subscription: Subscription, -} - -struct WatchedConnection { - server_name: SharedString, - messages: Vec, - list_state: ListState, - connection: Weak, - incoming_request_methods: HashMap>, - outgoing_request_methods: HashMap>, - _task: Task<()>, -} - -impl AcpTools { - fn new(project: Entity, cx: &mut Context) -> 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) { - 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) { - 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, - ) -> 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, - direction: acp::StreamMessageDirection, - message_type: MessageType, - params: Result, acp::Error>, - collapsed_params_md: Option>, - expanded_params_md: Option>, -} - -impl WatchedConnectionMessage { - fn expanded(&mut self, language_registry: Arc, 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, - cx: &mut App, -) -> Entity { - 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, - cx: &mut App, -) -> Entity { - 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 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 { - 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) -> 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(), - }) - } -} diff --git a/crates/action_log/Cargo.toml b/crates/action_log/Cargo.toml deleted file mode 100644 index 1a389e8859..0000000000 --- a/crates/action_log/Cargo.toml +++ /dev/null @@ -1,45 +0,0 @@ -[package] -name = "action_log" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lib] -path = "src/action_log.rs" - -[lints] -workspace = true - -[dependencies] -anyhow.workspace = true -buffer_diff.workspace = true -clock.workspace = true -collections.workspace = true -futures.workspace = true -gpui.workspace = true -language.workspace = true -project.workspace = true -text.workspace = true -util.workspace = true -watch.workspace = true -workspace-hack.workspace = true - - -[dev-dependencies] -buffer_diff = { workspace = true, features = ["test-support"] } -collections = { workspace = true, features = ["test-support"] } -clock = { workspace = true, features = ["test-support"] } -ctor.workspace = true -gpui = { workspace = true, features = ["test-support"] } -indoc.workspace = true -language = { workspace = true, features = ["test-support"] } -log.workspace = true -pretty_assertions.workspace = true -project = { workspace = true, features = ["test-support"] } -rand.workspace = true -serde_json.workspace = true -settings = { workspace = true, features = ["test-support"] } -text = { workspace = true, features = ["test-support"] } -util = { workspace = true, features = ["test-support"] } -zlog.workspace = true diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index 6641db0805..f8ea7173d8 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -103,21 +103,26 @@ impl ActivityIndicator { cx.subscribe_in( &workspace_handle, window, - |activity_indicator, _, event, window, cx| { - if let workspace::Event::ClearActivityIndicator = event - && activity_indicator.statuses.pop().is_some() - { - activity_indicator.dismiss_error_message(&DismissErrorMessage, window, cx); - cx.notify(); + |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| { - if let LspStoreEvent::LanguageServerUpdate { name, message, .. } = event { + |activity_indicator, _, event, cx| match event { + LspStoreEvent::LanguageServerUpdate { name, message, .. } => { if let proto::update_language_server::Variant::StatusUpdate(status_update) = message { @@ -186,6 +191,7 @@ impl ActivityIndicator { } cx.notify() } + _ => {} }, ) .detach(); @@ -200,10 +206,9 @@ impl ActivityIndicator { cx.subscribe( &project.read(cx).git_store().clone(), - |_, _, event: &GitStoreEvent, cx| { - if let project::git_store::GitStoreEvent::JobsUpdated = event { - cx.notify() - } + |_, _, event: &GitStoreEvent, cx| match event { + project::git_store::GitStoreEvent::JobsUpdated => cx.notify(), + _ => {} }, ) .detach(); @@ -453,24 +458,26 @@ 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 - && 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 { + 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, + }); + } } // Show any language server installation info. @@ -695,7 +702,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( @@ -707,13 +714,21 @@ 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 { + AutoUpdateStatus::Updated { + binary_path, + 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)), + on_click: Some(Arc::new({ + let reload = workspace::Reload { + binary_path: Some(binary_path.clone()), + }; + move |_, _, cx| workspace::reload(&reload, cx) + })), + tooltip_message: Some(Self::version_tooltip_message(&version)), }), AutoUpdateStatus::Errored => Some(Content { icon: Some( @@ -733,20 +748,21 @@ impl ActivityIndicator { if let Some(extension_store) = ExtensionStore::try_global(cx).map(|extension_store| extension_store.read(cx)) - && 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, - }); + 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, + }); + } } None diff --git a/crates/agent/Cargo.toml b/crates/agent/Cargo.toml index 391abb38fe..c89a7f3303 100644 --- a/crates/agent/Cargo.toml +++ b/crates/agent/Cargo.toml @@ -19,7 +19,6 @@ test-support = [ ] [dependencies] -action_log.workspace = true agent_settings.workspace = true anyhow.workspace = true assistant_context.workspace = true @@ -31,6 +30,7 @@ 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 @@ -47,6 +47,7 @@ paths.workspace = true postage.workspace = true project.workspace = true prompt_store.workspace = true +proto.workspace = true ref-cast.workspace = true rope.workspace = true schemars.workspace = true diff --git a/crates/agent/src/agent_profile.rs b/crates/agent/src/agent_profile.rs index c9e73372f6..34ea1c8df7 100644 --- a/crates/agent/src/agent_profile.rs +++ b/crates/agent/src/agent_profile.rs @@ -90,7 +90,7 @@ impl AgentProfile { return false; }; - Self::is_enabled(settings, source, tool_name) + return 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, tool_set); + let profile = AgentProfile::new(id.clone(), 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, tool_set); + let profile = AgentProfile::new(id.clone(), 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, tool_set); + let profile = AgentProfile::new(id.clone(), tool_set); let mut enabled_tools = cx .read(|cx| profile.enabled_tools(cx)) @@ -326,7 +326,7 @@ mod tests { _input: serde_json::Value, _request: Arc, _project: Entity, - _action_log: Entity, + _action_log: Entity, _model: Arc, _window: Option, _cx: &mut App, diff --git a/crates/agent/src/context.rs b/crates/agent/src/context.rs index a94a933d86..ddd13de491 100644 --- a/crates/agent/src/context.rs +++ b/crates/agent/src/context.rs @@ -20,7 +20,7 @@ use text::{Anchor, OffsetRangeExt as _}; use util::markdown::MarkdownCodeBlock; use util::{ResultExt as _, post_inc}; -pub const RULES_ICON: IconName = IconName::Reader; +pub const RULES_ICON: IconName = IconName::Context; pub enum ContextKind { File, @@ -40,10 +40,10 @@ impl ContextKind { ContextKind::File => IconName::File, ContextKind::Directory => IconName::Folder, ContextKind::Symbol => IconName::Code, - ContextKind::Selection => IconName::Reader, - ContextKind::FetchedUrl => IconName::ToolWeb, - ContextKind::Thread => IconName::Thread, - ContextKind::TextThread => IconName::TextThread, + ContextKind::Selection => IconName::Context, + ContextKind::FetchedUrl => IconName::Globe, + ContextKind::Thread => IconName::MessageBubbles, + ContextKind::TextThread => IconName::MessageBubbles, ContextKind::Rules => RULES_ICON, ContextKind::Image => IconName::Image, } @@ -201,24 +201,24 @@ impl FileContextHandle { parse_status.changed().await.log_err(); } - if let Ok(snapshot) = buffer.read_with(cx, |buffer, _| buffer.snapshot()) - && let Some(outline) = snapshot.outline(None) - { - let items = outline - .items - .into_iter() - .map(|item| item.to_point(&snapshot)); + 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])); + 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 +362,7 @@ impl Display for DirectoryContext { let mut is_first = true; for descendant in &self.descendants { if !is_first { - writeln!(f)?; + write!(f, "\n")?; } else { is_first = false; } @@ -650,7 +650,7 @@ impl TextThreadContextHandle { impl Display for TextThreadContext { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { // TODO: escape title? - writeln!(f, "", self.title)?; + write!(f, "\n", self.title)?; write!(f, "{}", self.text.trim())?; write!(f, "\n") } @@ -716,7 +716,7 @@ impl RulesContextHandle { impl Display for RulesContext { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { if let Some(title) = &self.title { - writeln!(f, "Rules title: {}", title)?; + write!(f, "Rules title: {}\n", title)?; } let code_block = MarkdownCodeBlock { tag: "", diff --git a/crates/agent/src/context_server_tool.rs b/crates/agent/src/context_server_tool.rs index 696c569356..85e8ac7451 100644 --- a/crates/agent/src/context_server_tool.rs +++ b/crates/agent/src/context_server_tool.rs @@ -1,8 +1,7 @@ use std::sync::Arc; -use action_log::ActionLog; use anyhow::{Result, anyhow, bail}; -use assistant_tool::{Tool, ToolResult, ToolSource}; +use assistant_tool::{ActionLog, Tool, ToolResult, ToolSource}; use context_server::{ContextServerId, types}; use gpui::{AnyWindowHandle, App, Entity, Task}; use icons::IconName; @@ -86,13 +85,15 @@ 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.client() else { + let Some(protocol) = server_clone.client() else { bail!("Context server not initialized"); }; - let arguments = if let serde_json::Value::Object(map) = input { + let arguments = if let serde_json::Value::Object(map) = input_clone { Some(map.into_iter().collect()) } else { None diff --git a/crates/agent/src/context_store.rs b/crates/agent/src/context_store.rs index b531852a18..60ba5527dc 100644 --- a/crates/agent/src/context_store.rs +++ b/crates/agent/src/context_store.rs @@ -338,9 +338,11 @@ impl ContextStore { image_task, context_id: self.next_context_id.post_inc(), }); - if self.has_context(&context) && remove_if_exists { - self.remove_context(&context, cx); - return None; + if self.has_context(&context) { + if remove_if_exists { + self.remove_context(&context, cx); + return None; + } } self.insert_context(context.clone(), cx); diff --git a/crates/agent/src/history_store.rs b/crates/agent/src/history_store.rs index 8f4c1a1e2e..89f75a72bd 100644 --- a/crates/agent/src/history_store.rs +++ b/crates/agent/src/history_store.rs @@ -212,16 +212,7 @@ impl HistoryStore { fn load_recently_opened_entries(cx: &AsyncApp) -> Task>> { cx.background_spawn(async move { let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH); - let contents = match smol::fs::read_to_string(path).await { - Ok(it) => it, - Err(e) if e.kind() == std::io::ErrorKind::NotFound => { - return Ok(Vec::new()); - } - Err(e) => { - return Err(e) - .context("deserializing persisted agent panel navigation history"); - } - }; + let contents = smol::fs::read_to_string(path).await?; let entries = serde_json::from_str::>(&contents) .context("deserializing persisted agent panel navigation history")? .into_iter() @@ -254,9 +245,10 @@ impl HistoryStore { } pub fn remove_recently_opened_thread(&mut self, id: ThreadId, cx: &mut Context) { - self.recently_opened_entries.retain( - |entry| !matches!(entry, HistoryEntryId::Thread(thread_id) if thread_id == &id), - ); + self.recently_opened_entries.retain(|entry| match entry { + HistoryEntryId::Thread(thread_id) if thread_id == &id => false, + _ => true, + }); self.save_recently_opened_entries(cx); } diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 7b70fde56a..ee16f83dc4 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -8,17 +8,14 @@ use crate::{ }, tool_use::{PendingToolUse, ToolUse, ToolUseMetadata, ToolUseState}, }; -use action_log::ActionLog; -use agent_settings::{ - AgentProfileId, AgentSettings, CompletionMode, SUMMARIZE_THREAD_DETAILED_PROMPT, - SUMMARIZE_THREAD_PROMPT, -}; +use agent_settings::{AgentProfileId, AgentSettings, CompletionMode}; use anyhow::{Result, anyhow}; -use assistant_tool::{AnyToolCard, Tool, ToolWorkingSet}; +use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet}; use chrono::{DateTime, Utc}; -use client::{ModelRequestUsage, RequestUsage}; -use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, Plan, UsageLimit}; +use client::{CloudUserStore, ModelRequestUsage, RequestUsage}; +use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit}; use collections::HashMap; +use feature_flags::{self, FeatureFlagAppExt}; use futures::{FutureExt, StreamExt as _, future::Shared}; use git::repository::DiffType; use gpui::{ @@ -40,6 +37,7 @@ use project::{ git_store::{GitStore, GitStoreCheckpoint, RepositoryState}, }; use prompt_store::{ModelContext, PromptBuilder}; +use proto::Plan; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::Settings; @@ -110,7 +108,7 @@ impl std::fmt::Display for PromptId { } #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)] -pub struct MessageId(pub usize); +pub struct MessageId(pub(crate) usize); impl MessageId { fn post_inc(&mut self) -> Self { @@ -181,7 +179,7 @@ impl Message { } } - pub fn to_message_content(&self) -> String { + pub fn to_string(&self) -> String { let mut result = String::new(); if !self.loaded_context.text.is_empty() { @@ -376,6 +374,7 @@ pub struct Thread { completion_count: usize, pending_completions: Vec, project: Entity, + cloud_user_store: Entity, prompt_builder: Arc, tools: Entity, tool_use: ToolUseState, @@ -387,8 +386,10 @@ pub struct Thread { cumulative_token_usage: TokenUsage, exceeded_window_error: Option, tool_use_limit_reached: bool, + feedback: Option, retry_state: Option, message_feedback: HashMap, + last_auto_capture_at: Option, last_received_chunk_at: Option, request_callback: Option< Box])>, @@ -444,6 +445,7 @@ pub struct ExceededWindowError { impl Thread { pub fn new( project: Entity, + cloud_user_store: Entity, tools: Entity, prompt_builder: Arc, system_prompt: SharedProjectContext, @@ -470,6 +472,7 @@ impl Thread { completion_count: 0, pending_completions: Vec::new(), project: project.clone(), + cloud_user_store, prompt_builder, tools: tools.clone(), last_restore_checkpoint: None, @@ -486,13 +489,15 @@ 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: configured_model.clone(), profile: AgentProfile::new(profile_id, tools), } } @@ -501,6 +506,7 @@ impl Thread { id: ThreadId, serialized: SerializedThread, project: Entity, + cloud_user_store: Entity, tools: Entity, prompt_builder: Arc, project_context: SharedProjectContext, @@ -530,7 +536,7 @@ impl Thread { .and_then(|model| { let model = SelectedModel { provider: model.provider.clone().into(), - model: model.model.into(), + model: model.model.clone().into(), }; registry.select_model(&model, cx) }) @@ -601,6 +607,7 @@ impl Thread { last_restore_checkpoint: None, pending_checkpoint: None, project: project.clone(), + cloud_user_store, prompt_builder, tools: tools.clone(), tool_use, @@ -610,7 +617,9 @@ 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, @@ -840,17 +849,11 @@ impl Thread { .await .unwrap_or(false); - 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, - }) - } - })?; + if !equal { + this.update(cx, |this, cx| { + this.insert_checkpoint(pending_checkpoint, cx) + })?; + } Ok(()) } @@ -1029,6 +1032,8 @@ impl Thread { }); } + self.auto_capture_telemetry(cx); + message_id } @@ -1643,15 +1648,17 @@ impl Thread { }; self.tool_use - .request_tool_use(tool_message_id, tool_use, tool_use_metadata, cx); + .request_tool_use(tool_message_id, tool_use, tool_use_metadata.clone(), cx); - self.tool_use.insert_tool_output( - tool_use_id, + let pending_tool_use = self.tool_use.insert_tool_output( + tool_use_id.clone(), tool_name, tool_output, self.configured_model.as_ref(), self.completion_mode, - ) + ); + + pending_tool_use } pub fn stream_completion( @@ -1684,7 +1691,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 { @@ -1816,7 +1823,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( @@ -1898,6 +1905,7 @@ impl Thread { cx.emit(ThreadEvent::StreamedCompletion); cx.notify(); + thread.auto_capture_telemetry(cx); Ok(()) })??; @@ -1965,9 +1973,11 @@ impl Thread { if let Some(prev_message) = thread.messages.get(ix - 1) - && prev_message.role == Role::Assistant { + { + if prev_message.role == Role::Assistant { break; } + } } } @@ -2040,7 +2050,7 @@ impl Thread { retry_scheduled = thread .handle_retryable_error_with_delay( - completion_error, + &completion_error, Some(retry_strategy), model.clone(), intent, @@ -2070,6 +2080,8 @@ 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; @@ -2106,10 +2118,12 @@ impl Thread { return; } + let added_user_message = include_str!("./prompts/summarize_thread_prompt.txt"); + let request = self.to_summarize_request( &model.model, CompletionIntent::ThreadSummarization, - SUMMARIZE_THREAD_PROMPT.into(), + added_user_message.into(), cx, ); @@ -2117,7 +2131,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 { @@ -2261,15 +2275,6 @@ impl Thread { max_attempts: 3, }) } - Other(err) - if err.is::() - || err.is::() => - { - // Retrying won't help for Payment Required or Model Request Limit errors (where - // the user must upgrade to usage-based billing to get more requests, or else wait - // for a significant amount of time for the request limit to reset). - None - } // Conservatively assume that any other errors are non-retryable HttpResponseError { .. } | Other(..) => Some(RetryStrategy::Fixed { delay: BASE_RETRY_DELAY, @@ -2425,10 +2430,12 @@ impl Thread { return; } + let added_user_message = include_str!("./prompts/summarize_thread_detailed_prompt.txt"); + let request = self.to_summarize_request( &model, CompletionIntent::ThreadContextSummarization, - SUMMARIZE_THREAD_DETAILED_PROMPT.into(), + added_user_message.into(), cx, ); @@ -2441,7 +2448,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| { @@ -2470,13 +2477,13 @@ impl Thread { .ok()?; // Save thread so its summary can be reused later - if let Some(thread) = thread.upgrade() - && let Ok(Ok(save_task)) = cx.update(|cx| { + if let Some(thread) = thread.upgrade() { + if 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(()) @@ -2521,6 +2528,7 @@ impl Thread { model: Arc, cx: &mut Context, ) -> Vec { + self.auto_capture_telemetry(cx); let request = Arc::new(self.to_completion_request(model.clone(), CompletionIntent::ToolResults, cx)); let pending_tool_uses = self @@ -2724,11 +2732,13 @@ impl Thread { window: Option, cx: &mut Context, ) { - 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); + 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); + } } cx.emit(ThreadEvent::ToolFinished { @@ -2784,6 +2794,10 @@ impl Thread { cx.emit(ThreadEvent::CancelEditing); } + pub fn feedback(&self) -> Option { + self.feedback + } + pub fn message_feedback(&self, message_id: MessageId) -> Option { self.message_feedback.get(&message_id).copied() } @@ -2816,7 +2830,7 @@ impl Thread { let message_content = self .message(message_id) - .map(|msg| msg.to_message_content()) + .map(|msg| msg.to_string()) .unwrap_or_default(); cx.background_spawn(async move { @@ -2845,6 +2859,52 @@ impl Thread { }) } + pub fn report_feedback( + &mut self, + feedback: ThreadFeedback, + cx: &mut Context, + ) -> Task> { + 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, @@ -2865,11 +2925,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() - && let Some(file) = buffer.file() - { - let path = file.path().to_string_lossy().to_string(); - unsaved_buffers.push(path); + if buffer.is_dirty() { + if let Some(file) = buffer.file() { + let path = file.path().to_string_lossy().to_string(); + unsaved_buffers.push(path); + } } } }) @@ -3079,6 +3139,50 @@ impl Thread { &self.project } + pub fn auto_capture_telemetry(&mut self, cx: &mut Context) { + if !cx.has_flag::() { + 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 } @@ -3121,13 +3225,13 @@ impl Thread { .model .max_token_count_for_mode(self.completion_mode().into()); - 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, - }); + 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, + }); + } } let total = self @@ -3156,18 +3260,15 @@ impl Thread { } fn update_model_request_usage(&self, amount: u32, limit: UsageLimit, cx: &mut Context) { - self.project - .read(cx) - .user_store() - .update(cx, |user_store, cx| { - user_store.update_model_request_usage( - ModelRequestUsage(RequestUsage { - amount: amount as i32, - limit, - }), - cx, - ) - }); + self.cloud_user_store.update(cx, |cloud_user_store, cx| { + cloud_user_store.update_model_request_usage( + ModelRequestUsage(RequestUsage { + amount: amount as i32, + limit, + }), + cx, + ) + }); } pub fn deny_tool_use( @@ -3188,7 +3289,7 @@ impl Thread { self.configured_model.as_ref(), self.completion_mode, ); - self.tool_finished(tool_use_id, None, true, window, cx); + self.tool_finished(tool_use_id.clone(), None, true, window, cx); } } @@ -3785,6 +3886,7 @@ fn main() {{ thread.id.clone(), serialized, thread.project.clone(), + thread.cloud_user_store.clone(), thread.tools.clone(), thread.prompt_builder.clone(), thread.project_context.clone(), @@ -3820,7 +3922,7 @@ fn main() {{ AgentSettings { model_parameters: vec![LanguageModelParameters { provider: Some(model.provider_id().0.to_string().into()), - model: Some(model.id().0), + model: Some(model.id().0.clone()), temperature: Some(0.66), }], ..AgentSettings::get_global(cx).clone() @@ -3840,7 +3942,7 @@ fn main() {{ AgentSettings { model_parameters: vec![LanguageModelParameters { provider: None, - model: Some(model.id().0), + model: Some(model.id().0.clone()), temperature: Some(0.66), }], ..AgentSettings::get_global(cx).clone() @@ -3880,7 +3982,7 @@ fn main() {{ AgentSettings { model_parameters: vec![LanguageModelParameters { provider: Some("anthropic".into()), - model: Some(model.id().0), + model: Some(model.id().0.clone()), temperature: Some(0.66), }], ..AgentSettings::get_global(cx).clone() @@ -3931,7 +4033,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, _| { @@ -3949,8 +4051,8 @@ fn main() {{ }); cx.run_until_parked(); - fake_model.send_last_completion_stream_text_chunk("Brief"); - fake_model.send_last_completion_stream_text_chunk(" Introduction"); + fake_model.stream_last_completion_response("Brief"); + fake_model.stream_last_completion_response(" Introduction"); fake_model.end_last_completion_stream(); cx.run_until_parked(); @@ -4026,7 +4128,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 @@ -4043,7 +4145,7 @@ fn main() {{ }); cx.run_until_parked(); - fake_model.send_last_completion_stream_text_chunk("A successful summary"); + fake_model.stream_last_completion_response("A successful summary"); fake_model.end_last_completion_stream(); cx.run_until_parked(); @@ -4676,7 +4778,7 @@ fn main() {{ !pending.is_empty(), "Should have a pending completion after retry" ); - fake_model.send_completion_stream_text_chunk(&pending[0], "Success!"); + fake_model.stream_completion_response(&pending[0], "Success!"); fake_model.end_completion_stream(&pending[0]); cx.run_until_parked(); @@ -4844,7 +4946,7 @@ fn main() {{ // Check for pending completions and complete them if let Some(pending) = inner_fake.pending_completions().first() { - inner_fake.send_completion_stream_text_chunk(pending, "Success!"); + inner_fake.stream_completion_response(pending, "Success!"); inner_fake.end_completion_stream(pending); } cx.run_until_parked(); @@ -5225,7 +5327,7 @@ fn main() {{ } #[gpui::test] - async fn test_retry_canceled_on_stop(cx: &mut TestAppContext) { + async fn test_retry_cancelled_on_stop(cx: &mut TestAppContext) { init_test_settings(cx); let project = create_test_project(cx, json!({})).await; @@ -5281,7 +5383,7 @@ fn main() {{ "Should have no pending completions after cancellation" ); - // Verify the retry was canceled by checking retry state + // Verify the retry was cancelled by checking retry state thread.read_with(cx, |thread, _| { if let Some(retry_state) = &thread.retry_state { panic!( @@ -5308,7 +5410,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)); @@ -5329,7 +5431,7 @@ fn main() {{ fn simulate_successful_response(fake_model: &FakeLanguageModel, cx: &mut TestAppContext) { cx.run_until_parked(); - fake_model.send_last_completion_stream_text_chunk("Assistant response"); + fake_model.stream_last_completion_response("Assistant response"); fake_model.end_last_completion_stream(); cx.run_until_parked(); } @@ -5381,10 +5483,16 @@ fn main() {{ let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (client, user_store) = + project.read_with(cx, |project, _cx| (project.client(), project.user_store())); + let cloud_user_store = + cx.new(|cx| CloudUserStore::new(client.cloud_client(), user_store, cx)); + let thread_store = cx .update(|_, cx| { ThreadStore::load( project.clone(), + cloud_user_store, cx.new(|_| ToolWorkingSet::default()), None, Arc::new(PromptBuilder::new(None).unwrap()), diff --git a/crates/agent/src/thread_store.rs b/crates/agent/src/thread_store.rs index cba2457566..6efa56f233 100644 --- a/crates/agent/src/thread_store.rs +++ b/crates/agent/src/thread_store.rs @@ -8,6 +8,7 @@ use agent_settings::{AgentProfileId, CompletionMode}; use anyhow::{Context as _, Result, anyhow}; use assistant_tool::{Tool, ToolId, ToolWorkingSet}; use chrono::{DateTime, Utc}; +use client::CloudUserStore; use collections::HashMap; use context_server::ContextServerId; use futures::{ @@ -42,7 +43,7 @@ use std::{ use util::ResultExt as _; pub static ZED_STATELESS: std::sync::LazyLock = - std::sync::LazyLock::new(|| std::env::var("ZED_STATELESS").is_ok_and(|v| !v.is_empty())); + std::sync::LazyLock::new(|| std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty())); #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum DataType { @@ -74,7 +75,7 @@ impl Column for DataType { } } -const RULES_FILE_NAMES: [&str; 9] = [ +const RULES_FILE_NAMES: [&'static str; 9] = [ ".rules", ".cursorrules", ".windsurfrules", @@ -104,6 +105,7 @@ pub type TextThreadStore = assistant_context::ContextStore; pub struct ThreadStore { project: Entity, + cloud_user_store: Entity, tools: Entity, prompt_builder: Arc, prompt_store: Option>, @@ -124,6 +126,7 @@ impl EventEmitter for ThreadStore {} impl ThreadStore { pub fn load( project: Entity, + cloud_user_store: Entity, tools: Entity, prompt_store: Option>, prompt_builder: Arc, @@ -133,8 +136,14 @@ impl ThreadStore { let (thread_store, ready_rx) = cx.update(|cx| { let mut option_ready_rx = None; let thread_store = cx.new(|cx| { - let (thread_store, ready_rx) = - Self::new(project, tools, prompt_builder, prompt_store, cx); + let (thread_store, ready_rx) = Self::new( + project, + cloud_user_store, + tools, + prompt_builder, + prompt_store, + cx, + ); option_ready_rx = Some(ready_rx); thread_store }); @@ -147,6 +156,7 @@ impl ThreadStore { fn new( project: Entity, + cloud_user_store: Entity, tools: Entity, prompt_builder: Arc, prompt_store: Option>, @@ -190,6 +200,7 @@ impl ThreadStore { let this = Self { project, + cloud_user_store, tools, prompt_builder, prompt_store, @@ -205,22 +216,6 @@ impl ThreadStore { (this, ready_rx) } - #[cfg(any(test, feature = "test-support"))] - pub fn fake(project: Entity, cx: &mut App) -> Self { - Self { - project, - tools: cx.new(|_| ToolWorkingSet::default()), - prompt_builder: Arc::new(PromptBuilder::new(None).unwrap()), - prompt_store: None, - context_server_tool_ids: HashMap::default(), - threads: Vec::new(), - project_context: SharedProjectContext::default(), - reload_system_prompt_tx: mpsc::channel(0).0, - _reload_system_prompt_task: Task::ready(()), - _subscriptions: vec![], - } - } - fn handle_project_event( &mut self, _project: Entity, @@ -423,6 +418,7 @@ impl ThreadStore { cx.new(|cx| { Thread::new( self.project.clone(), + self.cloud_user_store.clone(), self.tools.clone(), self.prompt_builder.clone(), self.project_context.clone(), @@ -441,6 +437,7 @@ impl ThreadStore { ThreadId::new(), serialized, self.project.clone(), + self.cloud_user_store.clone(), self.tools.clone(), self.prompt_builder.clone(), self.project_context.clone(), @@ -472,6 +469,7 @@ impl ThreadStore { id.clone(), thread, this.project.clone(), + this.cloud_user_store.clone(), this.tools.clone(), this.prompt_builder.clone(), this.project_context.clone(), @@ -581,32 +579,33 @@ impl ThreadStore { return; }; - if protocol.capable(context_server::protocol::ServerCapability::Tools) - && let Some(response) = protocol + if protocol.capable(context_server::protocol::ServerCapability::Tools) { + if let Some(response) = protocol .request::(()) .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 - }), - cx, - ) - }) - .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 + }), + 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(); + if let Some(tool_ids) = tool_ids { + this.update(cx, |this, _| { + this.context_server_tool_ids.insert(server_id, tool_ids); + }) + .log_err(); + } } } }) @@ -696,14 +695,13 @@ impl SerializedThreadV0_1_0 { let mut messages: Vec = Vec::with_capacity(self.0.messages.len()); for message in self.0.messages { - if message.role == Role::User - && !message.tool_results.is_empty() - && let Some(last_message) = messages.last_mut() - { - debug_assert!(last_message.role == Role::Assistant); + 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; + last_message.tool_results = message.tool_results; + continue; + } } messages.push(message); @@ -895,17 +893,6 @@ 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()) }; diff --git a/crates/agent/src/tool_use.rs b/crates/agent/src/tool_use.rs index 962dca591f..7392c0878d 100644 --- a/crates/agent/src/tool_use.rs +++ b/crates/agent/src/tool_use.rs @@ -112,13 +112,19 @@ impl ToolUseState { }, ); - 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); + 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); + } + } + } } } } @@ -131,7 +137,7 @@ impl ToolUseState { } pub fn cancel_pending(&mut self) -> Vec { - let mut canceled_tool_uses = Vec::new(); + let mut cancelled_tool_uses = Vec::new(); self.pending_tool_uses_by_id .retain(|tool_use_id, tool_use| { if matches!(tool_use.status, PendingToolUseStatus::Error { .. }) { @@ -149,10 +155,10 @@ impl ToolUseState { is_error: true, }, ); - canceled_tool_uses.push(tool_use.clone()); + cancelled_tool_uses.push(tool_use.clone()); false }); - canceled_tool_uses + cancelled_tool_uses } pub fn pending_tool_uses(&self) -> Vec<&PendingToolUse> { @@ -275,7 +281,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) - .is_some_and(|results| !results.is_empty()) + .map_or(false, |results| !results.is_empty()) } pub fn tool_result( diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml deleted file mode 100644 index 68246a96b0..0000000000 --- a/crates/agent2/Cargo.toml +++ /dev/null @@ -1,103 +0,0 @@ -[package] -name = "agent2" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -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 -http_client.workspace = true -indoc.workspace = true -itertools.workspace = true -language.workspace = true -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 -schemars.workspace = true -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 -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"] } -language_model = { workspace = true, "features" = ["test-support"] } -lsp = { workspace = true, "features" = ["test-support"] } -pretty_assertions.workspace = true -project = { workspace = true, "features" = ["test-support"] } -reqwest_client.workspace = true -settings = { workspace = true, "features" = ["test-support"] } -tempfile.workspace = true -terminal = { workspace = true, "features" = ["test-support"] } -theme = { workspace = true, "features" = ["test-support"] } -tree-sitter-rust.workspace = true -unindent = { workspace = true } -worktree = { workspace = true, "features" = ["test-support"] } -zlog.workspace = true diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs deleted file mode 100644 index 6fa36d33d5..0000000000 --- a/crates/agent2/src/agent.rs +++ /dev/null @@ -1,1423 +0,0 @@ -use crate::{ - ContextServerRegistry, Thread, ThreadEvent, ThreadsDatabase, ToolCallAuthorization, - UserMessageContent, templates::Templates, -}; -use crate::{HistoryStore, 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; -use futures::{StreamExt, future}; -use gpui::{ - App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity, -}; -use language_model::{LanguageModel, LanguageModelProvider, LanguageModelRegistry}; -use project::{Project, ProjectItem, ProjectPath, Worktree}; -use prompt_store::{ - ProjectContext, PromptId, PromptStore, RulesFileContext, UserRulesContext, WorktreeContext, -}; -use settings::update_settings_file; -use std::any::Any; -use std::collections::HashMap; -use std::path::Path; -use std::rc::Rc; -use std::sync::Arc; -use util::ResultExt; - -const RULES_FILE_NAMES: [&str; 9] = [ - ".rules", - ".cursorrules", - ".windsurfrules", - ".clinerules", - ".github/copilot-instructions.md", - "CLAUDE.md", - "AGENT.md", - "AGENTS.md", - "GEMINI.md", -]; - -pub struct RulesLoadingError { - pub message: SharedString, -} - -/// Holds both the internal Thread and the AcpThread for a session -struct Session { - /// The internal thread that processes messages - thread: Entity, - /// The ACP thread that handles protocol communication - acp_thread: WeakEntity, - pending_save: Task<()>, - _subscriptions: Vec, -} - -pub struct LanguageModels { - /// Access language model by ID - models: HashMap>, - /// Cached list for returning language model information - model_list: acp_thread::AgentModelList, - refresh_models_rx: watch::Receiver<()>, - refresh_models_tx: watch::Sender<()>, -} - -impl LanguageModels { - fn new(cx: &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, - }; - this.refresh_list(cx); - this - } - - fn refresh_list(&mut self, cx: &App) { - let providers = LanguageModelRegistry::global(cx) - .read(cx) - .providers() - .into_iter() - .filter(|provider| provider.is_authenticated(cx)) - .collect::>(); - - let mut language_model_list = IndexMap::default(); - let mut recommended_models = HashSet::default(); - - 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)); - } - } - if !recommended.is_empty() { - language_model_list.insert( - acp_thread::AgentModelGroupName("Recommended".into()), - recommended, - ); - } - - let mut models = HashMap::default(); - for provider in providers { - let mut provider_models = Vec::new(); - 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()) { - provider_models.push(model_info); - } - models.insert(model_id, model); - } - if !provider_models.is_empty() { - language_model_list.insert( - acp_thread::AgentModelGroupName(provider.name().0.clone()), - provider_models, - ); - } - } - - self.models = models; - self.model_list = acp_thread::AgentModelList::Grouped(language_model_list); - self.refresh_models_tx.send(()).ok(); - } - - fn watch(&self) -> watch::Receiver<()> { - self.refresh_models_rx.clone() - } - - pub fn model_from_id( - &self, - model_id: &acp_thread::AgentModelId, - ) -> Option> { - self.models.get(model_id).cloned() - } - - fn map_language_model_to_info( - model: &Arc, - provider: &Arc, - ) -> acp_thread::AgentModelInfo { - acp_thread::AgentModelInfo { - id: Self::model_id(model), - name: model.name().0, - icon: Some(provider.icon()), - } - } - - fn model_id(model: &Arc) -> acp_thread::AgentModelId { - acp_thread::AgentModelId(format!("{}/{}", model.provider_id().0, model.id().0).into()) - } -} - -pub struct NativeAgent { - /// Session ID -> Session mapping - sessions: HashMap, - history: Entity, - /// Shared project context for all threads - project_context: Entity, - project_context_needs_refresh: watch::Sender<()>, - _maintain_project_context: Task>, - context_server_registry: Entity, - /// Shared templates for all threads - templates: Arc, - /// Cached model information - models: LanguageModels, - project: Entity, - prompt_store: Option>, - fs: Arc, - _subscriptions: Vec, -} - -impl NativeAgent { - pub async fn new( - project: Entity, - history: Entity, - templates: Arc, - prompt_store: Option>, - fs: Arc, - cx: &mut AsyncApp, - ) -> Result> { - log::debug!("Creating new NativeAgent"); - - let project_context = cx - .update(|cx| Self::build_project_context(&project, prompt_store.as_ref(), cx))? - .await; - - cx.new(|cx| { - let mut subscriptions = vec![ - cx.subscribe(&project, Self::handle_project_event), - cx.subscribe( - &LanguageModelRegistry::global(cx), - Self::handle_models_updated_event, - ), - ]; - if let Some(prompt_store) = prompt_store.as_ref() { - subscriptions.push(cx.subscribe(prompt_store, Self::handle_prompts_updated_event)) - } - - let (project_context_needs_refresh_tx, project_context_needs_refresh_rx) = - watch::channel(()); - Self { - sessions: HashMap::new(), - 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 - }), - context_server_registry: cx.new(|cx| { - ContextServerRegistry::new(project.read(cx).context_server_store(), cx) - }), - templates, - models: LanguageModels::new(cx), - project, - prompt_store, - fs, - _subscriptions: subscriptions, - } - }) - } - - fn register_session( - &mut self, - thread_handle: Entity, - cx: &mut Context, - ) -> Entity { - let connection = Rc::new(NativeAgentConnection(cx.entity())); - 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(cx) - }); - - 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 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 - } - - async fn maintain_project_context( - this: WeakEntity, - mut needs_refresh: watch::Receiver<()>, - cx: &mut AsyncApp, - ) -> Result<()> { - while needs_refresh.changed().await.is_ok() { - let project_context = this - .update(cx, |this, cx| { - Self::build_project_context(&this.project, this.prompt_store.as_ref(), cx) - })? - .await; - this.update(cx, |this, cx| { - this.project_context = cx.new(|_| project_context); - })?; - } - - Ok(()) - } - - fn build_project_context( - project: &Entity, - prompt_store: Option<&Entity>, - cx: &mut App, - ) -> Task { - let worktrees = project.read(cx).visible_worktrees(cx).collect::>(); - let worktree_tasks = worktrees - .into_iter() - .map(|worktree| { - Self::load_worktree_info_for_system_prompt(worktree, project.clone(), cx) - }) - .collect::>(); - let default_user_rules_task = if let Some(prompt_store) = prompt_store.as_ref() { - prompt_store.read_with(cx, |prompt_store, cx| { - let prompts = prompt_store.default_prompt_metadata(); - let load_tasks = prompts.into_iter().map(|prompt_metadata| { - let contents = prompt_store.load(prompt_metadata.id, cx); - async move { (contents.await, prompt_metadata) } - }); - cx.background_spawn(future::join_all(load_tasks)) - }) - } else { - Task::ready(vec![]) - }; - - cx.spawn(async move |_cx| { - let (worktrees, default_user_rules) = - future::join(future::join_all(worktree_tasks), default_user_rules_task).await; - - let worktrees = worktrees - .into_iter() - .map(|(worktree, _rules_error)| { - // TODO: show error message - // if let Some(rules_error) = rules_error { - // this.update(cx, |_, cx| cx.emit(rules_error)).ok(); - // } - worktree - }) - .collect::>(); - - let default_user_rules = default_user_rules - .into_iter() - .flat_map(|(contents, prompt_metadata)| match contents { - Ok(contents) => Some(UserRulesContext { - uuid: match prompt_metadata.id { - PromptId::User { uuid } => uuid, - PromptId::EditWorkflow => return None, - }, - title: prompt_metadata.title.map(|title| title.to_string()), - contents, - }), - Err(_err) => { - // TODO: show error message - // this.update(cx, |_, cx| { - // cx.emit(RulesLoadingError { - // message: format!("{err:?}").into(), - // }); - // }) - // .ok(); - None - } - }) - .collect::>(); - - ProjectContext::new(worktrees, default_user_rules) - }) - } - - fn load_worktree_info_for_system_prompt( - worktree: Entity, - project: Entity, - cx: &mut App, - ) -> Task<(WorktreeContext, Option)> { - let tree = worktree.read(cx); - let root_name = tree.root_name().into(); - let abs_path = tree.abs_path(); - - let mut context = WorktreeContext { - root_name, - abs_path, - rules_file: None, - }; - - let rules_task = Self::load_worktree_rules_file(worktree, project, cx); - let Some(rules_task) = rules_task else { - return Task::ready((context, None)); - }; - - cx.spawn(async move |_| { - let (rules_file, rules_file_error) = match rules_task.await { - Ok(rules_file) => (Some(rules_file), None), - Err(err) => ( - None, - Some(RulesLoadingError { - message: format!("{err}").into(), - }), - ), - }; - context.rules_file = rules_file; - (context, rules_file_error) - }) - } - - fn load_worktree_rules_file( - worktree: Entity, - project: Entity, - cx: &mut App, - ) -> Option>> { - let worktree = worktree.read(cx); - let worktree_id = worktree.id(); - let selected_rules_file = RULES_FILE_NAMES - .into_iter() - .filter_map(|name| { - worktree - .entry_for_path(name) - .filter(|entry| entry.is_file()) - .map(|entry| entry.path.clone()) - }) - .next(); - - // Note that Cline supports `.clinerules` being a directory, but that is not currently - // supported. This doesn't seem to occur often in GitHub repositories. - selected_rules_file.map(|path_in_worktree| { - let project_path = ProjectPath { - worktree_id, - path: path_in_worktree.clone(), - }; - let buffer_task = - project.update(cx, |project, cx| project.open_buffer(project_path, cx)); - let rope_task = cx.spawn(async move |cx| { - buffer_task.await?.read_with(cx, |buffer, cx| { - let project_entry_id = buffer.entry_id(cx).context("buffer has no file")?; - anyhow::Ok((project_entry_id, buffer.as_rope().clone())) - })? - }); - // Build a string from the rope on a background thread. - cx.background_spawn(async move { - let (project_entry_id, rope) = rope_task.await?; - anyhow::Ok(RulesFileContext { - path_in_worktree, - text: rope.to_string().trim().to_string(), - project_entry_id: project_entry_id.to_usize(), - }) - }) - }) - } - - fn handle_thread_title_updated( - &mut self, - thread: Entity, - _: &TitleUpdated, - cx: &mut Context, - ) { - 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, - usage: &TokenUsageUpdated, - cx: &mut Context, - ) { - 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, - event: &project::Event, - _cx: &mut Context, - ) { - match event { - project::Event::WorktreeAdded(_) | project::Event::WorktreeRemoved(_) => { - self.project_context_needs_refresh.send(()).ok(); - } - project::Event::WorktreeUpdatedEntries(_, items) => { - if items.iter().any(|(path, _, _)| { - RULES_FILE_NAMES - .iter() - .any(|name| path.as_ref() == Path::new(name)) - }) { - self.project_context_needs_refresh.send(()).ok(); - } - } - _ => {} - } - } - - fn handle_prompts_updated_event( - &mut self, - _prompt_store: Entity, - _event: &prompt_store::PromptsUpdatedEvent, - _cx: &mut Context, - ) { - self.project_context_needs_refresh.send(()).ok(); - } - - fn handle_models_updated_event( - &mut self, - _registry: Entity, - _event: &language_model::Event, - cx: &mut Context, - ) { - 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, 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, - ) -> Task>> { - 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, - ) -> Task> { - 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, cx: &mut Context) { - 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); - -impl NativeAgentConnection { - pub fn thread(&self, session_id: &acp::SessionId, cx: &App) -> Option> { - 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, &mut App) -> Result>>, - ) -> Task> { - 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>, - acp_thread: WeakEntity, - cx: &App, - ) -> Task> { - 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 recv = acp_thread.update(cx, |thread, cx| { - thread.request_tool_call_authorization(tool_call, options, cx) - })?; - cx.background_spawn(async move { - if let Some(recv) = recv.log_err() - && let Some(option) = recv - .await - .context("authorization sender was dropped") - .log_err() - { - response - .send(option) - .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> { - log::debug!("NativeAgentConnection::list_models called"); - let list = self.0.read(cx).models.model_list.clone(); - Task::ready(if list.is_empty() { - Err(anyhow::anyhow!("No models available")) - } else { - Ok(list) - }) - } - - fn select_model( - &self, - session_id: acp::SessionId, - model_id: acp_thread::AgentModelId, - cx: &mut App, - ) -> Task> { - log::debug!("Setting model for session {}: {}", session_id, model_id); - let Some(thread) = self - .0 - .read(cx) - .sessions - .get(&session_id) - .map(|session| session.thread.clone()) - else { - return Task::ready(Err(anyhow!("Session not found"))); - }; - - let Some(model) = self.0.read(cx).models.model_from_id(&model_id) else { - return Task::ready(Err(anyhow!("Invalid model ID {}", model_id))); - }; - - thread.update(cx, |thread, cx| { - thread.set_model(model.clone(), cx); - }); - - update_settings_file::( - self.0.read(cx).fs.clone(), - cx, - move |settings, _cx| { - settings.set_model(model); - }, - ); - - Task::ready(Ok(())) - } - - fn selected_model( - &self, - session_id: &acp::SessionId, - cx: &mut App, - ) -> Task> { - let session_id = session_id.clone(); - - let Some(thread) = self - .0 - .read(cx) - .sessions - .get(&session_id) - .map(|session| session.thread.clone()) - else { - return Task::ready(Err(anyhow!("Session not found"))); - }; - 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, - ))) - } - - fn watch(&self, cx: &mut App) -> watch::Receiver<()> { - self.0.read(cx).models.watch() - } -} - -impl acp_thread::AgentConnection for NativeAgentConnection { - fn new_thread( - self: Rc, - project: Entity, - cwd: &Path, - cx: &mut App, - ) -> Task>> { - let agent = self.0.clone(); - log::debug!("Creating new thread for project at: {:?}", cwd); - - cx.spawn(async move |cx| { - log::debug!("Starting thread creation in async context"); - - // Create Thread - let thread = agent.update( - cx, - |agent, cx: &mut gpui::Context| -> 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(cx.new(|cx| { - Thread::new( - project.clone(), - agent.project_context.clone(), - agent.context_server_registry.clone(), - agent.templates.clone(), - default_model, - cx, - ) - })) - }, - )??; - agent.update(cx, |agent, cx| agent.register_session(thread, cx)) - }) - } - - fn auth_methods(&self) -> &[acp::AuthMethod] { - &[] // No auth for in-process - } - - fn authenticate(&self, _method: acp::AuthMethodId, _cx: &mut App) -> Task> { - Task::ready(Ok(())) - } - - fn model_selector(&self) -> Option> { - Some(Rc::new(self.clone()) as Rc) - } - - fn prompt( - &self, - id: Option, - params: acp::PromptRequest, - cx: &mut App, - ) -> Task> { - let id = id.expect("UserMessageId is required"); - let session_id = params.session_id.clone(); - log::info!("Received prompt request for session: {}", session_id); - log::debug!("Prompt blocks count: {}", params.prompt.len()); - - self.run_turn(session_id, cx, |thread, cx| { - let content: Vec = params - .prompt - .into_iter() - .map(Into::into) - .collect::>(); - log::debug!("Converted prompt to message: {} chars", content.len()); - log::debug!("Message id: {:?}", id); - log::debug!("Message content: {:?}", content); - - thread.update(cx, |thread, cx| thread.send(id, content, cx)) - }) - } - - fn resume( - &self, - session_id: &acp::SessionId, - _cx: &App, - ) -> Option> { - 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(cx)); - } - }); - } - - fn truncate( - &self, - session_id: &agent_client_protocol::SessionId, - cx: &App, - ) -> Option> { - self.0.read_with(cx, |agent, _cx| { - agent.sessions.get(session_id).map(|session| { - Rc::new(NativeAgentSessionEditor { - thread: session.thread.clone(), - acp_thread: session.acp_thread.clone(), - }) as _ - }) - }) - } - - fn set_title( - &self, - session_id: &acp::SessionId, - _cx: &App, - ) -> Option> { - Some(Rc::new(NativeAgentSessionSetTitle { - connection: self.clone(), - session_id: session_id.clone(), - }) as _) - } - - fn telemetry(&self) -> Option> { - Some(Rc::new(self.clone()) as Rc) - } - - fn into_any(self: Rc) -> Rc { - 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, - ) -> Task> { - 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 { - thread: Entity, - acp_thread: WeakEntity, -} - -impl acp_thread::AgentSessionTruncate for NativeAgentSessionEditor { - fn run(&self, message_id: acp_thread::UserMessageId, cx: &mut App) -> Task> { - 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> { - 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> { - 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(())) - } -} - -#[cfg(test)] -mod tests { - use crate::HistoryEntryId; - - use super::*; - 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) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/", - json!({ - "a": {} - }), - ) - .await; - let project = Project::test(fs.clone(), [], cx).await; - let context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); - let agent = NativeAgent::new( - project.clone(), - history_store, - Templates::new(), - None, - fs.clone(), - &mut cx.to_async(), - ) - .await - .unwrap(); - agent.read_with(cx, |agent, cx| { - assert_eq!(agent.project_context.read(cx).worktrees, vec![]) - }); - - let worktree = project - .update(cx, |project, cx| project.create_worktree("/a", true, cx)) - .await - .unwrap(); - cx.run_until_parked(); - agent.read_with(cx, |agent, cx| { - assert_eq!( - agent.project_context.read(cx).worktrees, - vec![WorktreeContext { - root_name: "a".into(), - abs_path: Path::new("/a").into(), - rules_file: None - }] - ) - }); - - // Creating `/a/.rules` updates the project context. - fs.insert_file("/a/.rules", Vec::new()).await; - cx.run_until_parked(); - agent.read_with(cx, |agent, cx| { - let rules_entry = worktree.read(cx).entry_for_path(".rules").unwrap(); - assert_eq!( - agent.project_context.read(cx).worktrees, - vec![WorktreeContext { - root_name: "a".into(), - abs_path: Path::new("/a").into(), - rules_file: Some(RulesFileContext { - path_in_worktree: Path::new(".rules").into(), - text: "".into(), - project_entry_id: rules_entry.id.to_usize() - }) - }] - ) - }); - } - - #[gpui::test] - async fn test_listing_models(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - fs.insert_tree("/", json!({ "a": {} })).await; - let project = Project::test(fs.clone(), [], cx).await; - let context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); - let connection = NativeAgentConnection( - NativeAgent::new( - project.clone(), - history_store, - Templates::new(), - None, - fs.clone(), - &mut cx.to_async(), - ) - .await - .unwrap(), - ); - - let models = cx.update(|cx| connection.list_models(cx)).await.unwrap(); - - let acp_thread::AgentModelList::Grouped(models) = models else { - panic!("Unexpected model group"); - }; - assert_eq!( - models, - IndexMap::from_iter([( - AgentModelGroupName("Fake".into()), - vec![AgentModelInfo { - id: AgentModelId("fake/fake".into()), - name: "Fake".into(), - icon: Some(ui::IconName::ZedAssistant), - }] - )]) - ); - } - - #[gpui::test] - async fn test_model_selection_persists_to_settings(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - fs.create_dir(paths::settings_file().parent().unwrap()) - .await - .unwrap(); - fs.insert_file( - paths::settings_file(), - json!({ - "agent": { - "default_model": { - "provider": "foo", - "model": "bar" - } - } - }) - .to_string() - .into_bytes(), - ) - .await; - let project = Project::test(fs.clone(), [], cx).await; - - let context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); - - // Create the agent and connection - let agent = NativeAgent::new( - project.clone(), - history_store, - Templates::new(), - None, - fs.clone(), - &mut cx.to_async(), - ) - .await - .unwrap(); - let connection = NativeAgentConnection(agent.clone()); - - // Create a thread/session - let acp_thread = cx - .update(|cx| { - Rc::new(connection.clone()).new_thread(project.clone(), Path::new("/a"), cx) - }) - .await - .unwrap(); - - let session_id = cx.update(|cx| acp_thread.read(cx).session_id().clone()); - - // Select a model - let model_id = AgentModelId("fake/fake".into()); - cx.update(|cx| connection.select_model(session_id.clone(), model_id.clone(), cx)) - .await - .unwrap(); - - // Verify the thread has the selected model - agent.read_with(cx, |agent, _| { - let session = agent.sessions.get(&session_id).unwrap(); - session.thread.read_with(cx, |thread, _| { - assert_eq!(thread.model().unwrap().id().0, "fake"); - }); - }); - - cx.run_until_parked(); - - // Verify settings file was updated - let settings_content = fs.load(paths::settings_file()).await.unwrap(); - let settings_json: serde_json::Value = serde_json::from_str(&settings_content).unwrap(); - - // Check that the agent settings contain the selected model - assert_eq!( - settings_json["agent"]["default_model"]["model"], - json!("fake") - ); - assert_eq!( - settings_json["agent"]["default_model"]["provider"], - json!("fake") - ); - } - - #[gpui::test] - #[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows - async fn test_save_load_thread(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/", - json!({ - "a": { - "b.md": "Lorem" - } - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/a").as_ref()], cx).await; - let context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); - let agent = NativeAgent::new( - project.clone(), - history_store.clone(), - Templates::new(), - None, - fs.clone(), - &mut cx.to_async(), - ) - .await - .unwrap(); - let connection = Rc::new(NativeAgentConnection(agent.clone())); - - let acp_thread = cx - .update(|cx| { - connection - .clone() - .new_thread(project.clone(), Path::new(""), cx) - }) - .await - .unwrap(); - let session_id = acp_thread.read_with(cx, |thread, _| thread.session_id().clone()); - let thread = agent.read_with(cx, |agent, _| { - agent.sessions.get(&session_id).unwrap().thread.clone() - }); - - // Ensure empty threads are not saved, even if they get mutated. - let model = Arc::new(FakeLanguageModel::default()); - let summary_model = Arc::new(FakeLanguageModel::default()); - thread.update(cx, |thread, cx| { - thread.set_model(model.clone(), cx); - thread.set_summarization_model(Some(summary_model.clone()), cx); - }); - cx.run_until_parked(); - assert_eq!(history_entries(&history_store, cx), vec![]); - - let send = acp_thread.update(cx, |thread, cx| { - thread.send( - vec![ - "What does ".into(), - acp::ContentBlock::ResourceLink(acp::ResourceLink { - name: "b.md".into(), - uri: MentionUri::File { - abs_path: path!("/a/b.md").into(), - } - .to_uri() - .to_string(), - annotations: None, - description: None, - mime_type: None, - size: None, - title: None, - }), - " mean?".into(), - ], - cx, - ) - }); - let send = cx.foreground_executor().spawn(send); - cx.run_until_parked(); - - model.send_last_completion_stream_text_chunk("Lorem."); - model.end_last_completion_stream(); - cx.run_until_parked(); - summary_model.send_last_completion_stream_text_chunk("Explaining /a/b.md"); - summary_model.end_last_completion_stream(); - - send.await.unwrap(); - acp_thread.read_with(cx, |thread, cx| { - assert_eq!( - thread.to_markdown(cx), - indoc! {" - ## User - - What does [@b.md](file:///a/b.md) mean? - - ## Assistant - - Lorem. - - "} - ) - }); - - cx.run_until_parked(); - - // Drop the ACP thread, which should cause the session to be dropped as well. - cx.update(|_| { - drop(thread); - drop(acp_thread); - }); - agent.read_with(cx, |agent, _| { - assert_eq!(agent.sessions.keys().cloned().collect::>(), []); - }); - - // Ensure the thread can be reloaded from disk. - assert_eq!( - history_entries(&history_store, cx), - vec![( - HistoryEntryId::AcpThread(session_id.clone()), - "Explaining /a/b.md".into() - )] - ); - let acp_thread = agent - .update(cx, |agent, cx| agent.open_thread(session_id.clone(), cx)) - .await - .unwrap(); - acp_thread.read_with(cx, |thread, cx| { - assert_eq!( - thread.to_markdown(cx), - indoc! {" - ## User - - What does [@b.md](file:///a/b.md) mean? - - ## Assistant - - Lorem. - - "} - ) - }); - } - - fn history_entries( - history: &Entity, - cx: &mut TestAppContext, - ) -> Vec<(HistoryEntryId, String)> { - history.read_with(cx, |history, _| { - history - .entries() - .map(|e| (e.id(), e.title().to_string())) - .collect::>() - }) - } - - 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); - agent_settings::init(cx); - language::init(cx); - LanguageModelRegistry::test(cx); - }); - } -} diff --git a/crates/agent2/src/agent2.rs b/crates/agent2/src/agent2.rs deleted file mode 100644 index 1fc9c1cb95..0000000000 --- a/crates/agent2/src/agent2.rs +++ /dev/null @@ -1,19 +0,0 @@ -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::*; -pub use tools::*; diff --git a/crates/agent2/src/db.rs b/crates/agent2/src/db.rs deleted file mode 100644 index e7d31c0c7a..0000000000 --- a/crates/agent2/src/db.rs +++ /dev/null @@ -1,499 +0,0 @@ -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}; - -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, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct DbThread { - pub title: SharedString, - pub messages: Vec, - pub updated_at: DateTime, - #[serde(default)] - pub detailed_summary: Option, - #[serde(default)] - pub initial_project_snapshot: Option>, - #[serde(default)] - pub cumulative_token_usage: language_model::TokenUsage, - #[serde(default)] - pub request_token_usage: HashMap, - #[serde(default)] - pub model: Option, - #[serde(default)] - pub completion_mode: Option, - #[serde(default)] - pub profile: Option, -} - -impl DbThread { - pub const VERSION: &'static str = "0.3.0"; - - pub fn from_json(json: &[u8]) -> Result { - let saved_thread_json = serde_json::from_slice::(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 { - 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, - }) - } -} - -pub static ZED_STATELESS: std::sync::LazyLock = - std::sync::LazyLock::new(|| std::env::var("ZED_STATELESS").is_ok_and(|v| !v.is_empty())); - -#[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 { - 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>, -} - -struct GlobalThreadsDatabase(Shared, Arc>>>); - -impl Global for GlobalThreadsDatabase {} - -impl ThreadsDatabase { - pub fn connect(cx: &mut App) -> Shared, Arc>>> { - if cx.has_global::() { - return cx.global::().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 { - 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>, - 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, String, String, DataType, Vec)>(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>> { - let connection = self.connection.clone(); - - self.executor.spawn(async move { - let connection = connection.lock(); - - let mut select = - connection.select_bound::<(), (Arc, 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>> { - let connection = self.connection.clone(); - - self.executor.spawn(async move { - let connection = connection.lock(); - let mut select = connection.select_bound::, (DataType, Vec)>(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> { - 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> { - let connection = self.connection.clone(); - - self.executor.spawn(async move { - let connection = connection.lock(); - - let mut delete = connection.exec_bound::>(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" - ); - } -} diff --git a/crates/agent2/src/history_store.rs b/crates/agent2/src/history_store.rs deleted file mode 100644 index c656456e01..0000000000 --- a/crates/agent2/src/history_store.rs +++ /dev/null @@ -1,357 +0,0 @@ -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 { - 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), -} - -impl Into 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, - entries: Vec, - context_store: Entity, - recently_opened_entries: VecDeque, - _subscriptions: Vec, - _save_recently_opened_entries_task: Task<()>, -} - -impl HistoryStore { - pub fn new( - context_store: Entity, - cx: &mut Context, - ) -> 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, - ) -> Task> { - 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, - cx: &mut Context, - ) -> Task> { - self.context_store.update(cx, |context_store, cx| { - context_store.delete_local_context(path, cx) - }) - } - - pub fn load_text_thread( - &self, - path: Arc, - cx: &mut Context, - ) -> Task>> { - self.context_store.update(cx, |context_store, cx| { - context_store.open_local_context(path, cx) - }) - } - - pub fn reload(&self, cx: &mut Context) { - 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) { - #[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 { - #[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) { - 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::>(); - - 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>> { - 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::>(&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.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.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, - cx: &mut Context, - ) { - 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.recently_opened_entries - .retain(|old_entry| old_entry != entry); - self.save_recently_opened_entries(cx); - } - - pub fn entries(&self) -> impl Iterator { - self.entries.iter().cloned() - } -} diff --git a/crates/agent2/src/native_agent_server.rs b/crates/agent2/src/native_agent_server.rs deleted file mode 100644 index 9ff98ccd18..0000000000 --- a/crates/agent2/src/native_agent_server.rs +++ /dev/null @@ -1,128 +0,0 @@ -use std::{any::Any, path::Path, rc::Rc, sync::Arc}; - -use agent_servers::AgentServer; -use anyhow::Result; -use fs::Fs; -use gpui::{App, Entity, SharedString, Task}; -use project::Project; -use prompt_store::PromptStore; - -use crate::{HistoryStore, NativeAgent, NativeAgentConnection, templates::Templates}; - -#[derive(Clone)] -pub struct NativeAgentServer { - fs: Arc, - history: Entity, -} - -impl NativeAgentServer { - pub fn new(fs: Arc, history: Entity) -> Self { - Self { fs, history } - } -} - -impl AgentServer for NativeAgentServer { - fn telemetry_id(&self) -> &'static str { - "zed" - } - - fn name(&self) -> SharedString { - "Zed Agent".into() - } - - fn empty_state_headline(&self) -> SharedString { - self.name() - } - - fn empty_state_message(&self) -> SharedString { - "".into() - } - - fn logo(&self) -> ui::IconName { - ui::IconName::ZedAgent - } - - fn connect( - &self, - _root_dir: &Path, - project: &Entity, - cx: &mut App, - ) -> Task>> { - log::debug!( - "NativeAgentServer::connect called for path: {:?}", - _root_dir - ); - let project = 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"); - let templates = Templates::new(); - let prompt_store = prompt_store.await?; - - log::debug!("Creating native agent entity"); - let agent = - NativeAgent::new(project, history, templates, Some(prompt_store), fs, cx).await?; - - // Create the connection wrapper - let connection = NativeAgentConnection(agent); - log::debug!("NativeAgentServer connection established successfully"); - - Ok(Rc::new(connection) as Rc) - }) - } - - fn into_any(self: Rc) -> Rc { - 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" - ); -} diff --git a/crates/agent2/src/templates.rs b/crates/agent2/src/templates.rs deleted file mode 100644 index 72a8f6633c..0000000000 --- a/crates/agent2/src/templates.rs +++ /dev/null @@ -1,87 +0,0 @@ -use anyhow::Result; -use gpui::SharedString; -use handlebars::Handlebars; -use rust_embed::RustEmbed; -use serde::Serialize; -use std::sync::Arc; - -#[derive(RustEmbed)] -#[folder = "src/templates"] -#[include = "*.hbs"] -struct Assets; - -pub struct Templates(Handlebars<'static>); - -impl Templates { - pub fn new() -> Arc { - let mut handlebars = Handlebars::new(); - handlebars.set_strict_mode(true); - handlebars.register_helper("contains", Box::new(contains)); - handlebars.register_embed_templates::().unwrap(); - Arc::new(Self(handlebars)) - } -} - -pub trait Template: Sized { - const TEMPLATE_NAME: &'static str; - - fn render(&self, templates: &Templates) -> Result - where - Self: Serialize + Sized, - { - Ok(templates.0.render(Self::TEMPLATE_NAME, self)?) - } -} - -#[derive(Serialize)] -pub struct SystemPromptTemplate<'a> { - #[serde(flatten)] - pub project: &'a prompt_store::ProjectContext, - pub available_tools: Vec, -} - -impl Template for SystemPromptTemplate<'_> { - const TEMPLATE_NAME: &'static str = "system_prompt.hbs"; -} - -/// Handlebars helper for checking if an item is in a list -fn contains( - h: &handlebars::Helper, - _: &handlebars::Handlebars, - _: &handlebars::Context, - _: &mut handlebars::RenderContext, - out: &mut dyn handlebars::Output, -) -> handlebars::HelperResult { - let list = h - .param(0) - .and_then(|v| v.value().as_array()) - .ok_or_else(|| { - handlebars::RenderError::new("contains: missing or invalid list parameter") - })?; - let query = h.param(1).map(|v| v.value()).ok_or_else(|| { - handlebars::RenderError::new("contains: missing or invalid query parameter") - })?; - - if list.contains(query) { - out.write("true")?; - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_system_prompt_template() { - let project = prompt_store::ProjectContext::default(); - let template = SystemPromptTemplate { - project: &project, - available_tools: vec!["echo".into()], - }; - let templates = Templates::new(); - let rendered = template.render(&templates).unwrap(); - assert!(rendered.contains("## Fixing Diagnostics")); - } -} diff --git a/crates/agent2/src/templates/system_prompt.hbs b/crates/agent2/src/templates/system_prompt.hbs deleted file mode 100644 index a9f67460d8..0000000000 --- a/crates/agent2/src/templates/system_prompt.hbs +++ /dev/null @@ -1,178 +0,0 @@ -You are a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices. - -## Communication - -1. Be conversational but professional. -2. Refer to the user in the second person and yourself in the first person. -3. Format your responses in markdown. Use backticks to format file, directory, function, and class names. -4. NEVER lie or make things up. -5. Refrain from apologizing all the time when results are unexpected. Instead, just try your best to proceed or explain the circumstances to the user without apologizing. - -{{#if (gt (len available_tools) 0)}} -## Tool Use - -1. Make sure to adhere to the tools schema. -2. Provide every required argument. -3. DO NOT use tools to access items that are already available in the context section. -4. Use only the tools that are currently available. -5. DO NOT use a tool that is not available just because it appears in the conversation. This means the user turned it off. -6. NEVER run commands that don't terminate on their own such as web servers (like `npm run start`, `npm run dev`, `python -m http.server`, etc) or file watchers. -7. Avoid HTML entity escaping - use plain characters instead. - -## Searching and Reading - -If you are unsure how to fulfill the user's request, gather more information with tool calls and/or clarifying questions. - -If appropriate, use tool calls to explore the current project, which contains the following root directories: - -{{#each worktrees}} -- `{{abs_path}}` -{{/each}} - -- Bias towards not asking the user for help if you can find the answer yourself. -- When providing paths to tools, the path should always start with the name of a project root directory listed above. -- Before you read or edit a file, you must first find the full path. DO NOT ever guess a file path! -{{# if (contains available_tools 'grep') }} -- When looking for symbols in the project, prefer the `grep` tool. -- As you learn about the structure of the project, use that information to scope `grep` searches to targeted subtrees of the project. -- The user might specify a partial file path. If you don't know the full path, use `find_path` (not `grep`) before you read the file. -{{/if}} -{{else}} -You are being tasked with providing a response, but you have no ability to use tools or to read or write any aspect of the user's system (other than any context the user might have provided to you). - -As such, if you need the user to perform any actions for you, you must request them explicitly. Bias towards giving a response to the best of your ability, and then making requests for the user to take action (e.g. to give you more context) only optionally. - -The one exception to this is if the user references something you don't know about - for example, the name of a source code file, function, type, or other piece of code that you have no awareness of. In this case, you MUST NOT MAKE SOMETHING UP, or assume you know what that thing is or how it works. Instead, you must ask the user for clarification rather than giving a response. -{{/if}} - -## Code Block Formatting - -Whenever you mention a code block, you MUST use ONLY use the following format: -```path/to/Something.blah#L123-456 -(code goes here) -``` -The `#L123-456` means the line number range 123 through 456, and the path/to/Something.blah -is a path in the project. (If there is no valid path in the project, then you can use -/dev/null/path.extension for its path.) This is the ONLY valid way to format code blocks, because the Markdown parser -does not understand the more common ```language syntax, or bare ``` blocks. It only -understands this path-based syntax, and if the path is missing, then it will error and you will have to do it over again. -Just to be really clear about this, if you ever find yourself writing three backticks followed by a language name, STOP! -You have made a mistake. You can only ever put paths after triple backticks! - -Based on all the information I've gathered, here's a summary of how this system works: -1. The README file is loaded into the system. -2. The system finds the first two headers, including everything in between. In this case, that would be: -```path/to/README.md#L8-12 -# First Header -This is the info under the first header. -## Sub-header -``` -3. Then the system finds the last header in the README: -```path/to/README.md#L27-29 -## Last Header -This is the last header in the README. -``` -4. Finally, it passes this information on to the next process. - - -In Markdown, hash marks signify headings. For example: -```/dev/null/example.md#L1-3 -# Level 1 heading -## Level 2 heading -### Level 3 heading -``` - -Here are examples of ways you must never render code blocks: - -In Markdown, hash marks signify headings. For example: -``` -# Level 1 heading -## Level 2 heading -### Level 3 heading -``` - -This example is unacceptable because it does not include the path. - -In Markdown, hash marks signify headings. For example: -```markdown -# Level 1 heading -## Level 2 heading -### Level 3 heading -``` - -This example is unacceptable because it has the language instead of the path. - -In Markdown, hash marks signify headings. For example: - # Level 1 heading - ## Level 2 heading - ### Level 3 heading - -This example is unacceptable because it uses indentation to mark the code block -instead of backticks with a path. - -In Markdown, hash marks signify headings. For example: -```markdown -/dev/null/example.md#L1-3 -# Level 1 heading -## Level 2 heading -### Level 3 heading -``` - -This example is unacceptable because the path is in the wrong place. The path must be directly after the opening backticks. - -{{#if (gt (len available_tools) 0)}} -## Fixing Diagnostics - -1. Make 1-2 attempts at fixing diagnostics, then defer to the user. -2. Never simplify code you've written just to solve diagnostics. Complete, mostly correct code is more valuable than perfect code that doesn't solve the problem. - -## Debugging - -When debugging, only make code changes if you are certain that you can solve the problem. -Otherwise, follow debugging best practices: -1. Address the root cause instead of the symptoms. -2. Add descriptive logging statements and error messages to track variable and code state. -3. Add test functions and statements to isolate the problem. - -{{/if}} -## Calling External APIs - -1. Unless explicitly requested by the user, use the best suited external APIs and packages to solve the task. There is no need to ask the user for permission. -2. When selecting which version of an API or package to use, choose one that is compatible with the user's dependency management file(s). If no such file exists or if the package is not present, use the latest version that is in your training data. -3. If an external API requires an API Key, be sure to point this out to the user. Adhere to best security practices (e.g. DO NOT hardcode an API key in a place where it can be exposed) - -## System Information - -Operating System: {{os}} -Default Shell: {{shell}} - -{{#if (or has_rules has_user_rules)}} -## User's Custom Instructions - -The following additional instructions are provided by the user, and should be followed to the best of your ability{{#if (gt (len available_tools) 0)}} without interfering with the tool use guidelines{{/if}}. - -{{#if has_rules}} -There are project rules that apply to these root directories: -{{#each worktrees}} -{{#if rules_file}} -`{{root_name}}/{{rules_file.path_in_worktree}}`: -`````` -{{{rules_file.text}}} -`````` -{{/if}} -{{/each}} -{{/if}} - -{{#if has_user_rules}} -The user has specified the following rules that should be applied: -{{#each user_rules}} - -{{#if title}} -Rules title: {{title}} -{{/if}} -`````` -{{contents}}} -`````` -{{/each}} -{{/if}} -{{/if}} diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs deleted file mode 100644 index fbeee46a48..0000000000 --- a/crates/agent2/src/tests/mod.rs +++ /dev/null @@ -1,2534 +0,0 @@ -use super::*; -use acp_thread::{AgentConnection, AgentModelGroupName, AgentModelList, UserMessageId}; -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::{ - 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, - 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 prompt_store::ProjectContext; -use reqwest_client::ReqwestClient; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use serde_json::json; -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] -async fn test_echo(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.send(UserMessageId::new(), ["Testing: Reply with 'Hello'"], cx) - }) - .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(), - indoc! {" - ## Assistant - - Hello - "} - ) - }); - assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]); -} - -#[gpui::test] -#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows -async fn test_thinking(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.send( - UserMessageId::new(), - [indoc! {" - Testing: - - Generate a thinking step where you just think the word 'Think', - and have your final answer be 'Hello' - "}], - cx, - ) - }) - .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(), - indoc! {" - ## Assistant - - Think - Hello - "} - ) - }); - assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]); -} - -#[gpui::test] -async fn test_system_prompt(cx: &mut TestAppContext) { - let ThreadTest { - model, - thread, - project_context, - .. - } = setup(cx, TestModel::Fake).await; - let fake_model = model.as_fake(); - - 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!( - pending_completions.len(), - 1, - "unexpected pending completions: {:?}", - pending_completions - ); - - let pending_completion = pending_completions.pop().unwrap(); - assert_eq!(pending_completion.messages[0].role, Role::System); - - let system_message = &pending_completion.messages[0]; - let system_prompt = system_message.content[0].to_str().unwrap(); - assert!( - system_prompt.contains("test-shell"), - "unexpected system message: {:?}", - system_message - ); - assert!( - system_prompt.contains("## Fixing Diagnostics"), - "unexpected system message: {:?}", - system_message - ); -} - -#[gpui::test] -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; - - // Test a tool call that's likely to complete *before* streaming stops. - let events = thread - .update(cx, |thread, cx| { - thread.add_tool(EchoTool); - thread.send( - UserMessageId::new(), - ["Now test the echo tool with 'Hello'. Does it work? Say 'Yes' or 'No'."], - cx, - ) - }) - .unwrap() - .collect() - .await; - assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]); - - // Test a tool calls that's likely to complete *after* streaming stops. - let events = thread - .update(cx, |thread, cx| { - thread.remove_tool(&EchoTool::name()); - thread.add_tool(DelayTool); - thread.send( - UserMessageId::new(), - [ - "Now call the delay tool with 200ms.", - "When the timer goes off, then you echo the output of the tool.", - ], - cx, - ) - }) - .unwrap() - .collect() - .await; - assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]); - thread.update(cx, |thread, _cx| { - assert!( - thread - .last_message() - .unwrap() - .as_agent_message() - .unwrap() - .content - .iter() - .any(|content| { - if let AgentMessageContent::Text(text) = content { - text.contains("Ding") - } else { - false - } - }), - "{}", - thread.to_markdown() - ); - }); -} - -#[gpui::test] -#[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) - }) - .unwrap(); - - let mut saw_partial_tool_use = false; - while let Some(event) = events.next().await { - 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(); - let agent_message = message.as_agent_message().unwrap(); - let last_content = agent_message.content.last().unwrap(); - if let AgentMessageContent::ToolUse(last_tool_use) = last_content { - assert_eq!(last_tool_use.name.as_ref(), "word_list"); - if tool_call.status == acp::ToolCallStatus::Pending { - if !last_tool_use.is_input_complete - && last_tool_use.input.get("g").is_none() - { - saw_partial_tool_use = true; - } - } else { - last_tool_use - .input - .get("a") - .expect("'a' has streamed because input is now complete"); - last_tool_use - .input - .get("g") - .expect("'g' has streamed because input is now complete"); - } - } else { - panic!("last content should be a tool use"); - } - }); - } - } - - assert!( - saw_partial_tool_use, - "should see at least one partially streamed tool use in the history" - ); -} - -#[gpui::test] -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) - }) - .unwrap(); - cx.run_until_parked(); - fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( - LanguageModelToolUse { - id: "tool_id_1".into(), - name: ToolRequiringPermission::name().into(), - raw_input: "{}".into(), - input: json!({}), - is_input_complete: true, - }, - )); - fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( - LanguageModelToolUse { - id: "tool_id_2".into(), - name: ToolRequiringPermission::name().into(), - raw_input: "{}".into(), - input: json!({}), - is_input_complete: true, - }, - )); - fake_model.end_last_completion_stream(); - let tool_call_auth_1 = next_tool_call_authorization(&mut events).await; - let tool_call_auth_2 = next_tool_call_authorization(&mut events).await; - - // Approve the first - tool_call_auth_1 - .response - .send(tool_call_auth_1.options[1].id.clone()) - .unwrap(); - cx.run_until_parked(); - - // Reject the second - tool_call_auth_2 - .response - .send(tool_call_auth_1.options[2].id.clone()) - .unwrap(); - cx.run_until_parked(); - - let completion = fake_model.pending_completions().pop().unwrap(); - let message = completion.messages.last().unwrap(); - assert_eq!( - message.content, - 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(), - 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(), - is_error: true, - content: "Permission to run tool denied by user".into(), - output: Some("Permission to run tool denied by user".into()) - }) - ] - ); - - // Simulate yet another tool call. - fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( - LanguageModelToolUse { - id: "tool_id_3".into(), - name: ToolRequiringPermission::name().into(), - raw_input: "{}".into(), - input: json!({}), - is_input_complete: true, - }, - )); - fake_model.end_last_completion_stream(); - - // Respond by always allowing tools. - let tool_call_auth_3 = next_tool_call_authorization(&mut events).await; - tool_call_auth_3 - .response - .send(tool_call_auth_3.options[0].id.clone()) - .unwrap(); - cx.run_until_parked(); - let completion = fake_model.pending_completions().pop().unwrap(); - let message = completion.messages.last().unwrap(); - assert_eq!( - message.content, - 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(), - is_error: false, - content: "Allowed".into(), - output: Some("Allowed".into()) - } - )] - ); - - // Simulate a final tool call, ensuring we don't trigger authorization. - fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( - LanguageModelToolUse { - id: "tool_id_4".into(), - name: ToolRequiringPermission::name().into(), - raw_input: "{}".into(), - input: json!({}), - is_input_complete: true, - }, - )); - fake_model.end_last_completion_stream(); - cx.run_until_parked(); - let completion = fake_model.pending_completions().pop().unwrap(); - let message = completion.messages.last().unwrap(); - assert_eq!( - message.content, - vec![language_model::MessageContent::ToolResult( - LanguageModelToolResult { - tool_use_id: "tool_id_4".into(), - tool_name: ToolRequiringPermission::name().into(), - is_error: false, - content: "Allowed".into(), - output: Some("Allowed".into()) - } - )] - ); -} - -#[gpui::test] -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) - }) - .unwrap(); - cx.run_until_parked(); - fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( - LanguageModelToolUse { - id: "tool_id_1".into(), - name: "nonexistent_tool".into(), - raw_input: "{}".into(), - input: json!({}), - is_input_complete: true, - }, - )); - fake_model.end_last_completion_stream(); - - let tool_call = expect_tool_call(&mut events).await; - assert_eq!(tool_call.title, "nonexistent_tool"); - assert_eq!(tool_call.status, acp::ToolCallStatus::Pending); - let update = expect_tool_call_update_fields(&mut events).await; - assert_eq!(update.fields.status, Some(acp::ToolCallStatus::Failed)); -} - -#[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::>().await.pop().unwrap(); - assert!( - last_event - .unwrap_err() - .is::() - ); - - 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::>().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::>().await.pop().unwrap(); - assert!( - last_event - .unwrap_err() - .is::() - ); - - 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>) -> acp::ToolCall { - let event = events - .next() - .await - .expect("no tool call authorization event received") - .unwrap(); - match event { - ThreadEvent::ToolCall(tool_call) => tool_call, - event => { - panic!("Unexpected event {event:?}"); - } - } -} - -async fn expect_tool_call_update_fields( - events: &mut UnboundedReceiver>, -) -> acp::ToolCallUpdate { - let event = events - .next() - .await - .expect("no tool call authorization event received") - .unwrap(); - match event { - ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(update)) => update, - event => { - panic!("Unexpected event {event:?}"); - } - } -} - -async fn next_tool_call_authorization( - events: &mut UnboundedReceiver>, -) -> ToolCallAuthorization { - loop { - let event = events - .next() - .await - .expect("no tool call authorization event received") - .unwrap(); - if let ThreadEvent::ToolCallAuthorization(tool_call_authorization) = event { - let permission_kinds = tool_call_authorization - .options - .iter() - .map(|o| o.kind) - .collect::>(); - assert_eq!( - permission_kinds, - vec![ - acp::PermissionOptionKind::AllowAlways, - acp::PermissionOptionKind::AllowOnce, - acp::PermissionOptionKind::RejectOnce, - ] - ); - return tool_call_authorization; - } - } -} - -#[gpui::test] -#[cfg_attr(not(feature = "e2e"), ignore)] -async fn test_concurrent_tool_calls(cx: &mut TestAppContext) { - let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await; - - // Test concurrent tool calls with different delay times - let events = thread - .update(cx, |thread, cx| { - thread.add_tool(DelayTool); - thread.send( - UserMessageId::new(), - [ - "Call the delay tool twice in the same message.", - "Once with 100ms. Once with 300ms.", - "When both timers are complete, describe the outputs.", - ], - cx, - ) - }) - .unwrap() - .collect() - .await; - - let stop_reasons = stop_events(events); - assert_eq!(stop_reasons, vec![acp::StopReason::EndTurn]); - - thread.update(cx, |thread, _cx| { - let last_message = thread.last_message().unwrap(); - let agent_message = last_message.as_agent_message().unwrap(); - let text = agent_message - .content - .iter() - .filter_map(|content| { - if let AgentMessageContent::Text(text) = content { - Some(text.as_str()) - } else { - None - } - }) - .collect::(); - - assert!(text.contains("Ding")); - }); -} - -#[gpui::test] -async fn test_profiles(cx: &mut TestAppContext) { - let ThreadTest { - model, thread, fs, .. - } = setup(cx, TestModel::Fake).await; - let fake_model = model.as_fake(); - - thread.update(cx, |thread, _cx| { - thread.add_tool(DelayTool); - thread.add_tool(EchoTool); - thread.add_tool(InfiniteTool); - }); - - // Override profiles and wait for settings to be loaded. - fs.insert_file( - paths::settings_file(), - json!({ - "agent": { - "profiles": { - "test-1": { - "name": "Test Profile 1", - "tools": { - EchoTool::name(): true, - DelayTool::name(): true, - } - }, - "test-2": { - "name": "Test Profile 2", - "tools": { - InfiniteTool::name(): true, - } - } - } - } - }) - .to_string() - .into_bytes(), - ) - .await; - 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) - }) - .unwrap(); - cx.run_until_parked(); - - let mut pending_completions = fake_model.pending_completions(); - assert_eq!(pending_completions.len(), 1); - let completion = pending_completions.pop().unwrap(); - let tool_names: Vec = completion - .tools - .iter() - .map(|tool| tool.name.clone()) - .collect(); - 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) - }) - .unwrap(); - cx.run_until_parked(); - let mut pending_completions = fake_model.pending_completions(); - assert_eq!(pending_completions.len(), 1); - let completion = pending_completions.pop().unwrap(); - let tool_names: Vec = completion - .tools - .iter() - .map(|tool| tool.name.clone()) - .collect(); - assert_eq!(tool_names, vec![InfiniteTool::name()]); -} - -#[gpui::test] -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(); - - // Override profiles and wait for settings to be loaded. - fs.insert_file( - paths::settings_file(), - json!({ - "agent": { - "profiles": { - "test": { - "name": "Test Profile", - "enable_all_context_servers": true, - "tools": { - EchoTool::name(): true, - } - }, - } - } - }) - .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::>().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::>().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(cx)); - let events = events.collect::>().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 - .update(cx, |thread, cx| { - thread.send( - UserMessageId::new(), - ["Testing: reply with 'Hello' then stop."], - cx, - ) - }) - .unwrap() - .collect::>() - .await; - thread.update(cx, |thread, _cx| { - let message = thread.last_message().unwrap(); - let agent_message = message.as_agent_message().unwrap(); - assert_eq!( - agent_message.content, - vec![AgentMessageContent::Text("Hello".to_string())] - ); - }); - assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]); -} - -#[gpui::test] -#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows -async fn test_in_progress_send_canceled_by_next_send(cx: &mut TestAppContext) { - let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; - let fake_model = model.as_fake(); - - let events_1 = thread - .update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Hello 1"], cx) - }) - .unwrap(); - cx.run_until_parked(); - fake_model.send_last_completion_stream_text_chunk("Hey 1!"); - cx.run_until_parked(); - - let events_2 = thread - .update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Hello 2"], cx) - }) - .unwrap(); - cx.run_until_parked(); - fake_model.send_last_completion_stream_text_chunk("Hey 2!"); - fake_model - .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)); - fake_model.end_last_completion_stream(); - - let events_1 = events_1.collect::>().await; - assert_eq!(stop_events(events_1), vec![acp::StopReason::Cancelled]); - let events_2 = events_2.collect::>().await; - assert_eq!(stop_events(events_2), vec![acp::StopReason::EndTurn]); -} - -#[gpui::test] -async fn test_subsequent_successful_sends_dont_cancel(cx: &mut TestAppContext) { - let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; - let fake_model = model.as_fake(); - - let events_1 = thread - .update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Hello 1"], cx) - }) - .unwrap(); - cx.run_until_parked(); - fake_model.send_last_completion_stream_text_chunk("Hey 1!"); - fake_model - .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)); - fake_model.end_last_completion_stream(); - let events_1 = events_1.collect::>().await; - - let events_2 = thread - .update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Hello 2"], cx) - }) - .unwrap(); - cx.run_until_parked(); - fake_model.send_last_completion_stream_text_chunk("Hey 2!"); - fake_model - .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)); - fake_model.end_last_completion_stream(); - let events_2 = events_2.collect::>().await; - - assert_eq!(stop_events(events_1), vec![acp::StopReason::EndTurn]); - assert_eq!(stop_events(events_2), vec![acp::StopReason::EndTurn]); -} - -#[gpui::test] -async fn test_refusal(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.send(UserMessageId::new(), ["Hello"], cx) - }) - .unwrap(); - cx.run_until_parked(); - thread.read_with(cx, |thread, _| { - assert_eq!( - thread.to_markdown(), - indoc! {" - ## User - - Hello - "} - ); - }); - - fake_model.send_last_completion_stream_text_chunk("Hey!"); - cx.run_until_parked(); - thread.read_with(cx, |thread, _| { - assert_eq!( - thread.to_markdown(), - indoc! {" - ## User - - Hello - - ## Assistant - - Hey! - "} - ); - }); - - // If the model refuses to continue, the thread should remove all the messages after the last user message. - fake_model - .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::Refusal)); - let events = events.collect::>().await; - assert_eq!(stop_events(events), vec![acp::StopReason::Refusal]); - thread.read_with(cx, |thread, _| { - assert_eq!(thread.to_markdown(), ""); - }); -} - -#[gpui::test] -async fn test_truncate_first_message(cx: &mut TestAppContext) { - let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; - let fake_model = model.as_fake(); - - let message_id = UserMessageId::new(); - thread - .update(cx, |thread, cx| { - thread.send(message_id.clone(), ["Hello"], cx) - }) - .unwrap(); - cx.run_until_parked(); - thread.read_with(cx, |thread, _| { - assert_eq!( - thread.to_markdown(), - indoc! {" - ## User - - Hello - "} - ); - assert_eq!(thread.latest_token_usage(), None); - }); - - fake_model.send_last_completion_stream_text_chunk("Hey!"); - fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate( - language_model::TokenUsage { - input_tokens: 32_000, - output_tokens: 16_000, - cache_creation_input_tokens: 0, - cache_read_input_tokens: 0, - }, - )); - cx.run_until_parked(); - thread.read_with(cx, |thread, _| { - assert_eq!( - thread.to_markdown(), - indoc! {" - ## User - - Hello - - ## Assistant - - Hey! - "} - ); - assert_eq!( - thread.latest_token_usage(), - Some(acp_thread::TokenUsage { - used_tokens: 32_000 + 16_000, - max_tokens: 1_000_000, - }) - ); - }); - - thread - .update(cx, |thread, cx| thread.truncate(message_id, cx)) - .unwrap(); - cx.run_until_parked(); - thread.read_with(cx, |thread, _| { - assert_eq!(thread.to_markdown(), ""); - assert_eq!(thread.latest_token_usage(), None); - }); - - // Ensure we can still send a new message after truncation. - thread - .update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Hi"], cx) - }) - .unwrap(); - thread.update(cx, |thread, _cx| { - assert_eq!( - thread.to_markdown(), - indoc! {" - ## User - - Hi - "} - ); - }); - cx.run_until_parked(); - fake_model.send_last_completion_stream_text_chunk("Ahoy!"); - fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate( - language_model::TokenUsage { - input_tokens: 40_000, - output_tokens: 20_000, - cache_creation_input_tokens: 0, - cache_read_input_tokens: 0, - }, - )); - cx.run_until_parked(); - thread.read_with(cx, |thread, _| { - assert_eq!( - thread.to_markdown(), - indoc! {" - ## User - - Hi - - ## Assistant - - Ahoy! - "} - ); - - assert_eq!( - thread.latest_token_usage(), - Some(acp_thread::TokenUsage { - used_tokens: 40_000 + 20_000, - max_tokens: 1_000_000, - }) - ); - }); -} - -#[gpui::test] -async fn test_truncate_second_message(cx: &mut TestAppContext) { - let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; - let fake_model = model.as_fake(); - - thread - .update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Message 1"], cx) - }) - .unwrap(); - cx.run_until_parked(); - fake_model.send_last_completion_stream_text_chunk("Message 1 response"); - fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate( - language_model::TokenUsage { - input_tokens: 32_000, - output_tokens: 16_000, - cache_creation_input_tokens: 0, - cache_read_input_tokens: 0, - }, - )); - fake_model.end_last_completion_stream(); - cx.run_until_parked(); - - let assert_first_message_state = |cx: &mut TestAppContext| { - thread.clone().read_with(cx, |thread, _| { - assert_eq!( - thread.to_markdown(), - indoc! {" - ## User - - Message 1 - - ## Assistant - - Message 1 response - "} - ); - - assert_eq!( - thread.latest_token_usage(), - Some(acp_thread::TokenUsage { - used_tokens: 32_000 + 16_000, - max_tokens: 1_000_000, - }) - ); - }); - }; - - assert_first_message_state(cx); - - let second_message_id = UserMessageId::new(); - thread - .update(cx, |thread, cx| { - thread.send(second_message_id.clone(), ["Message 2"], cx) - }) - .unwrap(); - cx.run_until_parked(); - - fake_model.send_last_completion_stream_text_chunk("Message 2 response"); - fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate( - language_model::TokenUsage { - input_tokens: 40_000, - output_tokens: 20_000, - cache_creation_input_tokens: 0, - cache_read_input_tokens: 0, - }, - )); - fake_model.end_last_completion_stream(); - cx.run_until_parked(); - - thread.read_with(cx, |thread, _| { - assert_eq!( - thread.to_markdown(), - indoc! {" - ## User - - Message 1 - - ## Assistant - - Message 1 response - - ## User - - Message 2 - - ## Assistant - - Message 2 response - "} - ); - - assert_eq!( - thread.latest_token_usage(), - Some(acp_thread::TokenUsage { - used_tokens: 40_000 + 20_000, - max_tokens: 1_000_000, - }) - ); - }); - - thread - .update(cx, |thread, cx| thread.truncate(second_message_id, cx)) - .unwrap(); - cx.run_until_parked(); - - assert_first_message_state(cx); -} - -#[gpui::test] -#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows -async fn test_title_generation(cx: &mut TestAppContext) { - let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; - let fake_model = model.as_fake(); - - let summary_model = Arc::new(FakeLanguageModel::default()); - thread.update(cx, |thread, cx| { - thread.set_summarization_model(Some(summary_model.clone()), cx) - }); - - let send = thread - .update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Hello"], cx) - }) - .unwrap(); - cx.run_until_parked(); - - fake_model.send_last_completion_stream_text_chunk("Hey!"); - fake_model.end_last_completion_stream(); - cx.run_until_parked(); - thread.read_with(cx, |thread, _| assert_eq!(thread.title(), "New Thread")); - - // Ensure the summary model has been invoked to generate a title. - summary_model.send_last_completion_stream_text_chunk("Hello "); - summary_model.send_last_completion_stream_text_chunk("world\nG"); - summary_model.send_last_completion_stream_text_chunk("oodnight Moon"); - summary_model.end_last_completion_stream(); - send.collect::>().await; - cx.run_until_parked(); - thread.read_with(cx, |thread, _| assert_eq!(thread.title(), "Hello world")); - - // Send another message, ensuring no title is generated this time. - let send = thread - .update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Hello again"], cx) - }) - .unwrap(); - cx.run_until_parked(); - fake_model.send_last_completion_stream_text_chunk("Hey again!"); - fake_model.end_last_completion_stream(); - cx.run_until_parked(); - assert_eq!(summary_model.pending_completions(), Vec::new()); - send.collect::>().await; - thread.read_with(cx, |thread, _| assert_eq!(thread.title(), "Hello world")); -} - -#[gpui::test] -async fn test_building_request_with_pending_tools(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(ToolRequiringPermission); - thread.add_tool(EchoTool); - thread.send(UserMessageId::new(), ["Hey!"], cx) - }) - .unwrap(); - cx.run_until_parked(); - - let permission_tool_use = LanguageModelToolUse { - id: "tool_id_1".into(), - name: ToolRequiringPermission::name().into(), - raw_input: "{}".into(), - input: json!({}), - is_input_complete: true, - }; - let echo_tool_use = LanguageModelToolUse { - id: "tool_id_2".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_text_chunk("Hi!"); - fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( - permission_tool_use, - )); - fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( - echo_tool_use.clone(), - )); - fake_model.end_last_completion_stream(); - cx.run_until_parked(); - - // Ensure pending tools are skipped when building a request. - let request = thread - .read_with(cx, |thread, cx| { - thread.build_completion_request(CompletionIntent::EditFile, cx) - }) - .unwrap(); - assert_eq!( - request.messages[1..], - vec![ - LanguageModelRequestMessage { - role: Role::User, - content: vec!["Hey!".into()], - cache: true - }, - LanguageModelRequestMessage { - role: Role::Assistant, - content: vec![ - MessageContent::Text("Hi!".into()), - MessageContent::ToolUse(echo_tool_use.clone()) - ], - cache: false - }, - LanguageModelRequestMessage { - role: Role::User, - content: vec![MessageContent::ToolResult(LanguageModelToolResult { - tool_use_id: echo_tool_use.id.clone(), - tool_name: echo_tool_use.name, - is_error: false, - content: "test".into(), - output: Some("test".into()) - })], - cache: false - }, - ], - ); -} - -#[gpui::test] -async fn test_agent_connection(cx: &mut TestAppContext) { - cx.update(settings::init); - let templates = Templates::new(); - - // Initialize language model system with test provider - cx.update(|cx| { - gpui_tokio::init(cx); - client::init_settings(cx); - - let http_client = FakeHttpClient::with_404_response(); - let clock = Arc::new(clock::FakeSystemClock::new()); - let client = Client::new(clock, http_client, cx); - let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); - language_model::init(client.clone(), cx); - language_models::init(user_store, client.clone(), cx); - Project::init_settings(cx); - LanguageModelRegistry::test(cx); - agent_settings::init(cx); - }); - cx.executor().forbid_parking(); - - // Create a project for new_thread - let fake_fs = cx.update(|cx| fs::FakeFs::new(cx.background_executor().clone())); - fake_fs.insert_tree(path!("/test"), json!({})).await; - let project = Project::test(fake_fs.clone(), [Path::new("/test")], cx).await; - let cwd = Path::new("/test"); - let context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); - - // Create agent and connection - let agent = NativeAgent::new( - project.clone(), - history_store, - templates.clone(), - None, - fake_fs.clone(), - &mut cx.to_async(), - ) - .await - .unwrap(); - let connection = NativeAgentConnection(agent.clone()); - - // Test model_selector returns Some - let selector_opt = connection.model_selector(); - assert!( - selector_opt.is_some(), - "agent2 should always support ModelSelector" - ); - let selector = selector_opt.unwrap(); - - // Test list_models - let listed_models = cx - .update(|cx| selector.list_models(cx)) - .await - .expect("list_models should succeed"); - let AgentModelList::Grouped(listed_models) = listed_models else { - panic!("Unexpected model list type"); - }; - assert!(!listed_models.is_empty(), "should have at least one model"); - assert_eq!( - listed_models[&AgentModelGroupName("Fake".into())][0].id.0, - "fake/fake" - ); - - // Create a thread using new_thread - let connection_rc = Rc::new(connection.clone()); - let acp_thread = cx - .update(|cx| connection_rc.new_thread(project, cwd, cx)) - .await - .expect("new_thread should succeed"); - - // Get the session_id from the AcpThread - let session_id = acp_thread.read_with(cx, |thread, _| thread.session_id().clone()); - - // Test selected_model returns the default - let model = cx - .update(|cx| selector.selected_model(&session_id, cx)) - .await - .expect("selected_model should succeed"); - let model = cx - .update(|cx| agent.read(cx).models().model_from_id(&model.id)) - .unwrap(); - let model = model.as_fake(); - assert_eq!(model.id().0, "fake", "should return default model"); - - let request = acp_thread.update(cx, |thread, cx| thread.send(vec!["abc".into()], cx)); - cx.run_until_parked(); - model.send_last_completion_stream_text_chunk("def"); - cx.run_until_parked(); - acp_thread.read_with(cx, |thread, cx| { - assert_eq!( - thread.to_markdown(cx), - indoc! {" - ## User - - abc - - ## Assistant - - def - - "} - ) - }); - - // Test cancel - cx.update(|cx| connection.cancel(&session_id, cx)); - request.await.expect("prompt should fail gracefully"); - - // Ensure that dropping the ACP thread causes the native thread to be - // dropped as well. - cx.update(|_| drop(acp_thread)); - let result = cx - .update(|cx| { - connection.prompt( - Some(acp_thread::UserMessageId::new()), - acp::PromptRequest { - session_id: session_id.clone(), - prompt: vec!["ghi".into()], - }, - cx, - ) - }) - .await; - assert_eq!( - result.as_ref().unwrap_err().to_string(), - "Session not found", - "unexpected result: {:?}", - result - ); -} - -#[gpui::test] -async fn test_tool_updates_to_completion(cx: &mut TestAppContext) { - let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await; - thread.update(cx, |thread, _cx| thread.add_tool(ThinkingTool)); - let fake_model = model.as_fake(); - - let mut events = thread - .update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Think"], cx) - }) - .unwrap(); - cx.run_until_parked(); - - // Simulate streaming partial input. - let input = json!({}); - fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( - LanguageModelToolUse { - id: "1".into(), - name: ThinkingTool::name().into(), - raw_input: input.to_string(), - input, - is_input_complete: false, - }, - )); - - // Input streaming completed - let input = json!({ "content": "Thinking hard!" }); - fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( - LanguageModelToolUse { - id: "1".into(), - name: "thinking".into(), - raw_input: input.to_string(), - input, - is_input_complete: true, - }, - )); - fake_model.end_last_completion_stream(); - cx.run_until_parked(); - - let tool_call = expect_tool_call(&mut events).await; - assert_eq!( - tool_call, - acp::ToolCall { - id: acp::ToolCallId("1".into()), - title: "Thinking".into(), - kind: acp::ToolKind::Think, - status: acp::ToolCallStatus::Pending, - content: vec![], - locations: vec![], - raw_input: Some(json!({})), - raw_output: None, - } - ); - let update = expect_tool_call_update_fields(&mut events).await; - assert_eq!( - update, - acp::ToolCallUpdate { - id: acp::ToolCallId("1".into()), - fields: acp::ToolCallUpdateFields { - title: Some("Thinking".into()), - kind: Some(acp::ToolKind::Think), - raw_input: Some(json!({ "content": "Thinking hard!" })), - ..Default::default() - }, - } - ); - let update = expect_tool_call_update_fields(&mut events).await; - assert_eq!( - update, - acp::ToolCallUpdate { - id: acp::ToolCallId("1".into()), - fields: acp::ToolCallUpdateFields { - status: Some(acp::ToolCallStatus::InProgress), - ..Default::default() - }, - } - ); - let update = expect_tool_call_update_fields(&mut events).await; - assert_eq!( - update, - acp::ToolCallUpdate { - id: acp::ToolCallId("1".into()), - fields: acp::ToolCallUpdateFields { - content: Some(vec!["Thinking hard!".into()]), - ..Default::default() - }, - } - ); - let update = expect_tool_call_update_fields(&mut events).await; - assert_eq!( - update, - acp::ToolCallUpdate { - id: acp::ToolCallId("1".into()), - fields: acp::ToolCallUpdateFields { - status: Some(acp::ToolCallStatus::Completed), - raw_output: Some("Finished thinking.".into()), - ..Default::default() - }, - } - ); -} - -#[gpui::test] -async fn test_send_no_retry_on_success(cx: &mut TestAppContext) { - let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await; - let fake_model = model.as_fake(); - - let mut events = thread - .update(cx, |thread, cx| { - thread.set_completion_mode(agent_settings::CompletionMode::Burn, cx); - thread.send(UserMessageId::new(), ["Hello!"], cx) - }) - .unwrap(); - cx.run_until_parked(); - - fake_model.send_last_completion_stream_text_chunk("Hey!"); - fake_model.end_last_completion_stream(); - - let mut retry_events = Vec::new(); - while let Some(Ok(event)) = events.next().await { - match event { - ThreadEvent::Retry(retry_status) => { - retry_events.push(retry_status); - } - ThreadEvent::Stop(..) => break, - _ => {} - } - } - - assert_eq!(retry_events.len(), 0); - thread.read_with(cx, |thread, _cx| { - assert_eq!( - thread.to_markdown(), - indoc! {" - ## User - - Hello! - - ## Assistant - - Hey! - "} - ) - }); -} - -#[gpui::test] -async fn test_send_retry_on_error(cx: &mut TestAppContext) { - let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await; - let fake_model = model.as_fake(); - - let mut events = thread - .update(cx, |thread, cx| { - thread.set_completion_mode(agent_settings::CompletionMode::Burn, cx); - thread.send(UserMessageId::new(), ["Hello!"], cx) - }) - .unwrap(); - cx.run_until_parked(); - - fake_model.send_last_completion_stream_text_chunk("Hey,"); - fake_model.send_last_completion_stream_error(LanguageModelCompletionError::ServerOverloaded { - provider: LanguageModelProviderName::new("Anthropic"), - retry_after: Some(Duration::from_secs(3)), - }); - fake_model.end_last_completion_stream(); - - cx.executor().advance_clock(Duration::from_secs(3)); - cx.run_until_parked(); - - fake_model.send_last_completion_stream_text_chunk("there!"); - fake_model.end_last_completion_stream(); - cx.run_until_parked(); - - let mut retry_events = Vec::new(); - while let Some(Ok(event)) = events.next().await { - match event { - ThreadEvent::Retry(retry_status) => { - retry_events.push(retry_status); - } - ThreadEvent::Stop(..) => break, - _ => {} - } - } - - assert_eq!(retry_events.len(), 1); - assert!(matches!( - retry_events[0], - acp_thread::RetryStatus { attempt: 1, .. } - )); - thread.read_with(cx, |thread, _cx| { - assert_eq!( - thread.to_markdown(), - indoc! {" - ## User - - Hello! - - ## Assistant - - Hey, - - [resume] - - ## Assistant - - there! - "} - ) - }); -} - -#[gpui::test] -async fn test_send_retry_finishes_tool_calls_on_error(cx: &mut TestAppContext) { - let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await; - let fake_model = model.as_fake(); - - let events = thread - .update(cx, |thread, cx| { - thread.set_completion_mode(agent_settings::CompletionMode::Burn, cx); - thread.add_tool(EchoTool); - thread.send(UserMessageId::new(), ["Call the echo tool!"], cx) - }) - .unwrap(); - cx.run_until_parked(); - - let tool_use_1 = 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_1.clone(), - )); - fake_model.send_last_completion_stream_error(LanguageModelCompletionError::ServerOverloaded { - provider: LanguageModelProviderName::new("Anthropic"), - retry_after: Some(Duration::from_secs(3)), - }); - fake_model.end_last_completion_stream(); - - cx.executor().advance_clock(Duration::from_secs(3)); - let completion = fake_model.pending_completions().pop().unwrap(); - assert_eq!( - completion.messages[1..], - vec![ - LanguageModelRequestMessage { - role: Role::User, - content: vec!["Call the echo tool!".into()], - cache: false - }, - LanguageModelRequestMessage { - role: Role::Assistant, - content: vec![language_model::MessageContent::ToolUse(tool_use_1.clone())], - cache: false - }, - LanguageModelRequestMessage { - role: Role::User, - content: vec![language_model::MessageContent::ToolResult( - LanguageModelToolResult { - tool_use_id: tool_use_1.id.clone(), - tool_name: tool_use_1.name.clone(), - is_error: false, - content: "test".into(), - output: Some("test".into()) - } - )], - cache: true - }, - ] - ); - - fake_model.send_last_completion_stream_text_chunk("Done"); - fake_model.end_last_completion_stream(); - cx.run_until_parked(); - events.collect::>().await; - thread.read_with(cx, |thread, _cx| { - assert_eq!( - thread.last_message(), - Some(Message::Agent(AgentMessage { - content: vec![AgentMessageContent::Text("Done".into())], - tool_results: IndexMap::default() - })) - ); - }) -} - -#[gpui::test] -async fn test_send_max_retries_exceeded(cx: &mut TestAppContext) { - let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await; - let fake_model = model.as_fake(); - - let mut events = thread - .update(cx, |thread, cx| { - thread.set_completion_mode(agent_settings::CompletionMode::Burn, cx); - thread.send(UserMessageId::new(), ["Hello!"], cx) - }) - .unwrap(); - cx.run_until_parked(); - - for _ in 0..crate::thread::MAX_RETRY_ATTEMPTS + 1 { - fake_model.send_last_completion_stream_error( - LanguageModelCompletionError::ServerOverloaded { - provider: LanguageModelProviderName::new("Anthropic"), - retry_after: Some(Duration::from_secs(3)), - }, - ); - fake_model.end_last_completion_stream(); - cx.executor().advance_clock(Duration::from_secs(3)); - cx.run_until_parked(); - } - - let mut errors = Vec::new(); - let mut retry_events = Vec::new(); - while let Some(event) = events.next().await { - match event { - Ok(ThreadEvent::Retry(retry_status)) => { - retry_events.push(retry_status); - } - Ok(ThreadEvent::Stop(..)) => break, - Err(error) => errors.push(error), - _ => {} - } - } - - assert_eq!( - retry_events.len(), - crate::thread::MAX_RETRY_ATTEMPTS as usize - ); - for i in 0..crate::thread::MAX_RETRY_ATTEMPTS as usize { - assert_eq!(retry_events[i].attempt, i + 1); - } - assert_eq!(errors.len(), 1); - let error = errors[0] - .downcast_ref::() - .unwrap(); - assert!(matches!( - error, - LanguageModelCompletionError::ServerOverloaded { .. } - )); -} - -/// Filters out the stop events for asserting against in tests -fn stop_events(result_events: Vec>) -> Vec { - result_events - .into_iter() - .filter_map(|event| match event.unwrap() { - ThreadEvent::Stop(stop_reason) => Some(stop_reason), - _ => None, - }) - .collect() -} - -struct ThreadTest { - model: Arc, - thread: Entity, - project_context: Entity, - context_server_store: Entity, - fs: Arc, -} - -enum TestModel { - Sonnet4, - Fake, -} - -impl TestModel { - fn id(&self) -> LanguageModelId { - match self { - TestModel::Sonnet4 => LanguageModelId("claude-sonnet-4-latest".into()), - TestModel::Fake => unreachable!(), - } - } -} - -async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest { - cx.executor().allow_parking(); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.create_dir(paths::settings_file().parent().unwrap()) - .await - .unwrap(); - fs.insert_file( - paths::settings_file(), - json!({ - "agent": { - "default_profile": "test-profile", - "profiles": { - "test-profile": { - "name": "Test Profile", - "tools": { - EchoTool::name(): true, - DelayTool::name(): true, - WordListTool::name(): true, - ToolRequiringPermission::name(): true, - InfiniteTool::name(): true, - ThinkingTool::name(): true, - } - } - } - } - }) - .to_string() - .into_bytes(), - ) - .await; - - cx.update(|cx| { - settings::init(cx); - Project::init_settings(cx); - agent_settings::init(cx); - gpui_tokio::init(cx); - let http_client = ReqwestClient::user_agent("agent tests").unwrap(); - cx.set_http_client(Arc::new(http_client)); - - client::init_settings(cx); - let client = Client::production(cx); - let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); - language_model::init(client.clone(), cx); - language_models::init(user_store, client.clone(), cx); - - watch_settings(fs.clone(), cx); - }); - - let templates = Templates::new(); - - fs.insert_tree(path!("/test"), json!({})).await; - let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await; - - let model = cx - .update(|cx| { - if let TestModel::Fake = model { - Task::ready(Arc::new(FakeLanguageModel::default()) as Arc<_>) - } else { - let model_id = model.id(); - let models = LanguageModelRegistry::read_global(cx); - let model = models - .available_models(cx) - .find(|model| model.id() == model_id) - .unwrap(); - - let provider = models.provider(&model.provider_id()).unwrap(); - let authenticated = provider.authenticate(cx); - - cx.spawn(async move |_cx| { - authenticated.await.unwrap(); - model - }) - } - }) - .await; - - let project_context = cx.new(|_cx| ProjectContext::default()); - let context_server_store = project.read_with(cx, |project, _| project.context_server_store()); - let context_server_registry = - cx.new(|cx| ContextServerRegistry::new(context_server_store.clone(), cx)); - let thread = cx.new(|cx| { - Thread::new( - project, - project_context.clone(), - context_server_registry, - templates, - Some(model.clone()), - cx, - ) - }); - ThreadTest { - model, - thread, - project_context, - context_server_store, - fs, - } -} - -#[cfg(test)] -#[ctor::ctor] -fn init_logger() { - if std::env::var("RUST_LOG").is_ok() { - env_logger::init(); - } -} - -fn watch_settings(fs: Arc, cx: &mut App) { - let fs = fs.clone(); - cx.spawn({ - async move |cx| { - let mut new_settings_content_rx = settings::watch_config_file( - cx.background_executor(), - fs, - paths::settings_file().clone(), - ); - - while let Some(new_settings_content) = new_settings_content_rx.next().await { - cx.update(|cx| { - SettingsStore::update_global(cx, |settings, cx| { - settings.set_user_settings(&new_settings_content, cx) - }) - }) - .ok(); - } - } - }) - .detach(); -} - -fn tool_names_for_completion(completion: &LanguageModelRequest) -> Vec { - completion - .tools - .iter() - .map(|tool| tool.name.clone()) - .collect() -} - -fn setup_context_server( - name: &'static str, - tools: Vec, - context_server_store: &Entity, - cx: &mut TestAppContext, -) -> mpsc::UnboundedReceiver<( - context_server::types::CallToolParams, - oneshot::Sender, -)> { - cx.update(|cx| { - let mut settings = ProjectSettings::get_global(cx).clone(); - settings.context_servers.insert( - name.into(), - project::project_settings::ContextServerSettings::Custom { - enabled: true, - command: ContextServerCommand { - path: "somebinary".into(), - args: Vec::new(), - env: None, - }, - }, - ); - ProjectSettings::override_global(settings, cx); - }); - - let (mcp_tool_calls_tx, mcp_tool_calls_rx) = mpsc::unbounded(); - let fake_transport = context_server::test::create_fake_transport(name, cx.executor()) - .on_request::(move |_params| async move { - context_server::types::InitializeResponse { - protocol_version: context_server::types::ProtocolVersion( - context_server::types::LATEST_PROTOCOL_VERSION.to_string(), - ), - server_info: context_server::types::Implementation { - name: name.into(), - version: "1.0.0".to_string(), - }, - capabilities: context_server::types::ServerCapabilities { - tools: Some(context_server::types::ToolsCapabilities { - list_changed: Some(true), - }), - ..Default::default() - }, - meta: None, - } - }) - .on_request::(move |_params| { - let tools = tools.clone(); - async move { - context_server::types::ListToolsResponse { - tools, - next_cursor: None, - meta: None, - } - } - }) - .on_request::(move |params| { - let mcp_tool_calls_tx = mcp_tool_calls_tx.clone(); - async move { - let (response_tx, response_rx) = oneshot::channel(); - mcp_tool_calls_tx - .unbounded_send((params, response_tx)) - .unwrap(); - response_rx.await.unwrap() - } - }); - context_server_store.update(cx, |store, cx| { - store.start_server( - Arc::new(ContextServer::new( - ContextServerId(name.into()), - Arc::new(fake_transport), - )), - cx, - ); - }); - cx.run_until_parked(); - mcp_tool_calls_rx -} diff --git a/crates/agent2/src/tests/test_tools.rs b/crates/agent2/src/tests/test_tools.rs deleted file mode 100644 index 27be7b6ac3..0000000000 --- a/crates/agent2/src/tests/test_tools.rs +++ /dev/null @@ -1,201 +0,0 @@ -use super::*; -use anyhow::Result; -use gpui::{App, SharedString, Task}; -use std::future; - -/// A tool that echoes its input -#[derive(JsonSchema, Serialize, Deserialize)] -pub struct EchoToolInput { - /// The text to echo. - pub text: String, -} - -pub struct EchoTool; - -impl AgentTool for EchoTool { - type Input = EchoToolInput; - type Output = String; - - fn name() -> &'static str { - "echo" - } - - fn kind() -> acp::ToolKind { - acp::ToolKind::Other - } - - fn initial_title(&self, _input: Result) -> SharedString { - "Echo".into() - } - - fn run( - self: Arc, - input: Self::Input, - _event_stream: ToolCallEventStream, - _cx: &mut App, - ) -> Task> { - Task::ready(Ok(input.text)) - } -} - -/// A tool that waits for a specified delay -#[derive(JsonSchema, Serialize, Deserialize)] -pub struct DelayToolInput { - /// The delay in milliseconds. - ms: u64, -} - -pub struct DelayTool; - -impl AgentTool for DelayTool { - type Input = DelayToolInput; - type Output = String; - - fn name() -> &'static str { - "delay" - } - - fn initial_title(&self, input: Result) -> SharedString { - if let Ok(input) = input { - format!("Delay {}ms", input.ms).into() - } else { - "Delay".into() - } - } - - fn kind() -> acp::ToolKind { - acp::ToolKind::Other - } - - fn run( - self: Arc, - input: Self::Input, - _event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task> - where - Self: Sized, - { - cx.foreground_executor().spawn(async move { - smol::Timer::after(Duration::from_millis(input.ms)).await; - Ok("Ding".to_string()) - }) - } -} - -#[derive(JsonSchema, Serialize, Deserialize)] -pub struct ToolRequiringPermissionInput {} - -pub struct ToolRequiringPermission; - -impl AgentTool for ToolRequiringPermission { - type Input = ToolRequiringPermissionInput; - type Output = String; - - fn name() -> &'static str { - "tool_requiring_permission" - } - - fn kind() -> acp::ToolKind { - acp::ToolKind::Other - } - - fn initial_title(&self, _input: Result) -> SharedString { - "This tool requires permission".into() - } - - fn run( - self: Arc, - _input: Self::Input, - event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task> { - let authorize = event_stream.authorize("Authorize?", cx); - cx.foreground_executor().spawn(async move { - authorize.await?; - Ok("Allowed".to_string()) - }) - } -} - -#[derive(JsonSchema, Serialize, Deserialize)] -pub struct InfiniteToolInput {} - -pub struct InfiniteTool; - -impl AgentTool for InfiniteTool { - type Input = InfiniteToolInput; - type Output = String; - - fn name() -> &'static str { - "infinite" - } - - fn kind() -> acp::ToolKind { - acp::ToolKind::Other - } - - fn initial_title(&self, _input: Result) -> SharedString { - "Infinite Tool".into() - } - - fn run( - self: Arc, - _input: Self::Input, - _event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task> { - cx.foreground_executor().spawn(async move { - future::pending::<()>().await; - unreachable!() - }) - } -} - -/// A tool that takes an object with map from letters to random words starting with that letter. -/// All fiealds are required! Pass a word for every letter! -#[derive(JsonSchema, Serialize, Deserialize)] -pub struct WordListInput { - /// Provide a random word that starts with A. - a: Option, - /// Provide a random word that starts with B. - b: Option, - /// Provide a random word that starts with C. - c: Option, - /// Provide a random word that starts with D. - d: Option, - /// Provide a random word that starts with E. - e: Option, - /// Provide a random word that starts with F. - f: Option, - /// Provide a random word that starts with G. - g: Option, -} - -pub struct WordListTool; - -impl AgentTool for WordListTool { - type Input = WordListInput; - type Output = String; - - fn name() -> &'static str { - "word_list" - } - - fn kind() -> acp::ToolKind { - acp::ToolKind::Other - } - - fn initial_title(&self, _input: Result) -> SharedString { - "List of random words".into() - } - - fn run( - self: Arc, - _input: Self::Input, - _event_stream: ToolCallEventStream, - _cx: &mut App, - ) -> Task> { - Task::ready(Ok("ok".to_string())) - } -} diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs deleted file mode 100644 index 97ea1caf1d..0000000000 --- a/crates/agent2/src/thread.rs +++ /dev/null @@ -1,2615 +0,0 @@ -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, AgentProfileSettings, AgentSettings, CompletionMode, - SUMMARIZE_THREAD_DETAILED_PROMPT, SUMMARIZE_THREAD_PROMPT, -}; -use anyhow::{Context as _, Result, anyhow}; -use assistant_tool::adapt_schema_to_format; -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 git::repository::DiffType; -use gpui::{ - App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity, -}; -use language_model::{ - 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 prompt_store::ProjectContext; -use schemars::{JsonSchema, Schema}; -use serde::{Deserialize, Serialize}; -use settings::{Settings, update_settings_file}; -use smol::stream::StreamExt; -use std::fmt::Write; -use std::{ - collections::BTreeMap, - ops::RangeInclusive, - path::Path, - sync::Arc, - time::{Duration, Instant}, -}; -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); - -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, Serialize, Deserialize)] -pub enum Message { - User(UserMessage), - Agent(AgentMessage), - Resume, -} - -impl Message { - pub fn as_agent_message(&self) -> Option<&AgentMessage> { - match self { - Message::Agent(agent_message) => Some(agent_message), - _ => None, - } - } - - pub fn to_request(&self) -> Vec { - 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, Serialize, Deserialize)] -pub struct UserMessage { - pub id: UserMessageId, - pub content: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum UserMessageContent { - Text(String), - Mention { uri: MentionUri, content: String }, - Image(LanguageModelImage), -} - -impl UserMessage { - pub fn to_markdown(&self) -> String { - let mut markdown = String::from("## User\n\n"); - - for content in &self.content { - match content { - UserMessageContent::Text(text) => { - markdown.push_str(text); - markdown.push('\n'); - } - UserMessageContent::Image(_) => { - markdown.push_str("\n"); - } - UserMessageContent::Mention { uri, content } => { - if !content.is_empty() { - let _ = writeln!(&mut markdown, "{}\n\n{}", uri.as_link(), content); - } else { - let _ = writeln!(&mut markdown, "{}", uri.as_link()); - } - } - } - } - - markdown - } - - fn to_request(&self) -> LanguageModelRequestMessage { - let mut message = LanguageModelRequestMessage { - role: Role::User, - content: Vec::with_capacity(self.content.len()), - cache: false, - }; - - const OPEN_CONTEXT: &str = "\n\ - The following items were attached by the user. \ - They are up-to-date and don't need to be re-read.\n\n"; - - const OPEN_FILES_TAG: &str = ""; - const OPEN_DIRECTORIES_TAG: &str = ""; - const OPEN_SYMBOLS_TAG: &str = ""; - const OPEN_SELECTIONS_TAG: &str = ""; - const OPEN_THREADS_TAG: &str = ""; - const OPEN_FETCH_TAG: &str = ""; - const OPEN_RULES_TAG: &str = - "\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(); - - for chunk in &self.content { - let chunk = match chunk { - UserMessageContent::Text(text) => { - language_model::MessageContent::Text(text.clone()) - } - UserMessageContent::Image(value) => { - language_model::MessageContent::Image(value.clone()) - } - UserMessageContent::Mention { uri, content } => { - match uri { - MentionUri::File { abs_path } => { - write!( - &mut file_context, - "\n{}", - MarkdownCodeBlock { - 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 { - abs_path: path, - line_range, - .. - } => { - write!( - &mut symbol_context, - "\n{}", - MarkdownCodeBlock { - tag: &codeblock_tag(path, Some(line_range)), - text: content - } - ) - .ok(); - } - MentionUri::Selection { - abs_path: path, - line_range, - .. - } => { - write!( - &mut selection_context, - "\n{}", - MarkdownCodeBlock { - tag: &codeblock_tag( - path.as_deref().unwrap_or("Untitled".as_ref()), - Some(line_range) - ), - text: content - } - ) - .ok(); - } - MentionUri::Thread { .. } => { - write!(&mut thread_context, "\n{}\n", content).ok(); - } - MentionUri::TextThread { .. } => { - write!(&mut thread_context, "\n{}\n", content).ok(); - } - MentionUri::Rule { .. } => { - write!( - &mut rules_context, - "\n{}", - MarkdownCodeBlock { - tag: "", - text: content - } - ) - .ok(); - } - MentionUri::Fetch { url } => { - write!(&mut fetch_context, "\nFetch: {}\n\n{}", url, content).ok(); - } - } - - language_model::MessageContent::Text(uri.as_link().to_string()) - } - }; - - message.content.push(chunk); - } - - let len_before_context = message.content.len(); - - if file_context.len() > OPEN_FILES_TAG.len() { - file_context.push_str("\n"); - message - .content - .push(language_model::MessageContent::Text(file_context)); - } - - if directory_context.len() > OPEN_DIRECTORIES_TAG.len() { - directory_context.push_str("\n"); - message - .content - .push(language_model::MessageContent::Text(directory_context)); - } - - if symbol_context.len() > OPEN_SYMBOLS_TAG.len() { - symbol_context.push_str("\n"); - message - .content - .push(language_model::MessageContent::Text(symbol_context)); - } - - if selection_context.len() > OPEN_SELECTIONS_TAG.len() { - selection_context.push_str("\n"); - message - .content - .push(language_model::MessageContent::Text(selection_context)); - } - - if thread_context.len() > OPEN_THREADS_TAG.len() { - thread_context.push_str("\n"); - message - .content - .push(language_model::MessageContent::Text(thread_context)); - } - - if fetch_context.len() > OPEN_FETCH_TAG.len() { - fetch_context.push_str("\n"); - message - .content - .push(language_model::MessageContent::Text(fetch_context)); - } - - if rules_context.len() > OPEN_RULES_TAG.len() { - rules_context.push_str("\n"); - message - .content - .push(language_model::MessageContent::Text(rules_context)); - } - - if message.content.len() > len_before_context { - message.content.insert( - len_before_context, - language_model::MessageContent::Text(OPEN_CONTEXT.into()), - ); - message - .content - .push(language_model::MessageContent::Text("".into())); - } - - message - } -} - -fn codeblock_tag(full_path: &Path, line_range: Option<&RangeInclusive>) -> String { - let mut result = String::new(); - - if let Some(extension) = full_path.extension().and_then(|ext| ext.to_str()) { - let _ = write!(result, "{} ", extension); - } - - let _ = write!(result, "{}", full_path.display()); - - if let Some(range) = line_range { - if range.start() == range.end() { - let _ = write!(result, ":{}", range.start() + 1); - } else { - let _ = write!(result, ":{}-{}", range.start() + 1, range.end() + 1); - } - } - - result -} - -impl AgentMessage { - pub fn to_markdown(&self) -> String { - let mut markdown = String::from("## Assistant\n\n"); - - for content in &self.content { - match content { - AgentMessageContent::Text(text) => { - markdown.push_str(text); - markdown.push('\n'); - } - AgentMessageContent::Thinking { text, .. } => { - markdown.push_str(""); - markdown.push_str(text); - markdown.push_str("\n"); - } - AgentMessageContent::RedactedThinking(_) => { - markdown.push_str("\n") - } - AgentMessageContent::ToolUse(tool_use) => { - markdown.push_str(&format!( - "**Tool Use**: {} (ID: {})\n", - tool_use.name, tool_use.id - )); - markdown.push_str(&format!( - "{}\n", - MarkdownCodeBlock { - tag: "json", - text: &format!("{:#}", tool_use.input) - } - )); - } - } - } - - for tool_result in self.tool_results.values() { - markdown.push_str(&format!( - "**Tool Result**: {} (ID: {})\n\n", - tool_result.tool_name, tool_result.tool_use_id - )); - if tool_result.is_error { - markdown.push_str("**ERROR:**\n"); - } - - match &tool_result.content { - LanguageModelToolResultContent::Text(text) => { - writeln!(markdown, "{text}\n").ok(); - } - LanguageModelToolResultContent::Image(_) => { - writeln!(markdown, "\n").ok(); - } - } - - if let Some(output) = tool_result.output.as_ref() { - writeln!( - markdown, - "**Debug Output**:\n\n```json\n{}\n```\n", - serde_json::to_string_pretty(output).unwrap() - ) - .unwrap(); - } - } - - markdown - } - - pub fn to_request(&self) -> Vec { - let mut assistant_message = LanguageModelRequestMessage { - role: Role::Assistant, - content: Vec::with_capacity(self.content.len()), - cache: false, - }; - for chunk in &self.content { - match chunk { - AgentMessageContent::Text(text) => { - assistant_message - .content - .push(language_model::MessageContent::Text(text.clone())); - } - AgentMessageContent::Thinking { text, signature } => { - assistant_message - .content - .push(language_model::MessageContent::Thinking { - text: text.clone(), - signature: signature.clone(), - }); - } - AgentMessageContent::RedactedThinking(value) => { - assistant_message.content.push( - language_model::MessageContent::RedactedThinking(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())); - } - } - }; - } - - let mut user_message = LanguageModelRequestMessage { - role: Role::User, - content: Vec::new(), - cache: false, - }; - - for tool_result in self.tool_results.values() { - user_message - .content - .push(language_model::MessageContent::ToolResult( - tool_result.clone(), - )); - } - - 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, Serialize, Deserialize)] -pub struct AgentMessage { - pub content: Vec, - pub tool_results: IndexMap, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum AgentMessageContent { - Text(String), - Thinking { - text: String, - signature: Option, - }, - RedactedThinking(String), - ToolUse(LanguageModelToolUse), -} - -#[derive(Debug)] -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 ToolCallAuthorization { - pub tool_call: acp::ToolCallUpdate, - pub options: Vec, - pub response: oneshot::Sender, -} - -#[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, - title: Option, - pending_title_generation: Option>, - summary: Option, - messages: Vec, - 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, - pending_message: Option, - tools: BTreeMap>, - tool_use_limit_reached: bool, - request_token_usage: HashMap, - #[allow(unused)] - cumulative_token_usage: TokenUsage, - #[allow(unused)] - initial_project_snapshot: Shared>>>, - context_server_registry: Entity, - profile_id: AgentProfileId, - project_context: Entity, - templates: Arc, - model: Option>, - summarization_model: Option>, - prompt_capabilities_tx: watch::Sender, - pub(crate) prompt_capabilities_rx: watch::Receiver, - pub(crate) project: Entity, - pub(crate) action_log: Entity, -} - -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_context: Entity, - context_server_registry: Entity, - templates: Arc, - model: Option>, - cx: &mut Context, - ) -> 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: 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, - ) -> mpsc::UnboundedReceiver> { - 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, - ) { - 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()); - 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_context: Entity, - context_server_registry: Entity, - action_log: Entity, - templates: Arc, - cx: &mut Context, - ) -> 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, - 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 { - 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, - cx: &mut Context, - ) -> Task> { - 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, - git_store: Entity, - cx: &App, - ) -> Task { - 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 { - &self.project_context - } - - pub fn project(&self) -> &Entity { - &self.project - } - - pub fn action_log(&self) -> &Entity { - &self.action_log - } - - pub fn is_empty(&self) -> bool { - self.messages.is_empty() && self.title.is_none() - } - - pub fn model(&self) -> Option<&Arc> { - self.model.as_ref() - } - - pub fn set_model(&mut self, model: Arc, cx: &mut Context) { - 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> { - self.summarization_model.as_ref() - } - - pub fn set_summarization_model( - &mut self, - model: Option>, - cx: &mut Context, - ) { - 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) { - 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"))] - pub fn last_message(&self) -> Option { - if let Some(message) = self.pending_message.clone() { - Some(Message::Agent(message)) - } else { - self.messages.last().cloned() - } - } - - pub fn add_default_tools(&mut self, cx: &mut Context) { - 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(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(), cx)); - self.add_tool(ThinkingTool); - self.add_tool(WebSearchTool); - } - - pub fn add_tool(&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, cx: &mut Context) { - 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) { - 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, cx: &mut Context) -> 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")); - }; - - 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 { - 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, - ) -> Result>> { - 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( - &mut self, - id: UserMessageId, - content: impl IntoIterator, - cx: &mut Context, - ) -> Result>> - where - T: Into, - { - 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::>(); - log::debug!("Thread::send content: {:?}", content); - - self.messages - .push(Message::User(UserMessage { id, content })); - cx.notify(); - - log::debug!("Total messages in thread: {}", self.messages.len()); - self.run_turn(cx) - } - - fn run_turn( - &mut self, - cx: &mut Context, - ) -> Result>> { - 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::>(); - 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::() { - Ok(CompletionError::Refusal) => { - event_stream.send_stop(acp::StopReason::Refusal); - _ = this.update(cx, |this, _| this.messages.truncate(message_ix)); - } - Ok(CompletionError::MaxTokens) => { - event_stream.send_stop(acp::StopReason::MaxTokens); - } - Ok(CompletionError::Other(error)) | Err(error) => { - event_stream.send_error(error); - } - } - } - } - - _ = this.update(cx, |this, _| this.running_turn.take()); - }), - }); - Ok(events_rx) - } - - async fn run_turn_internal( - this: &WeakEntity, - model: Arc, - 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) - })??); - } - Err(err) => { - error = Some(err); - break; - } - } - } - - 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 { - intent = CompletionIntent::ToolResults; - attempt = 0; - } - } - } - - fn handle_completion_error( - &mut self, - error: LanguageModelCompletionError, - attempt: u8, - ) -> Result { - if self.completion_mode == CompletionMode::Normal { - return Err(anyhow!(error)); - } - - 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 will - /// send back to the model when it resolves. - fn handle_completion_event( - &mut self, - event: LanguageModelCompletionEvent, - event_stream: &ThreadEventStream, - cx: &mut Context, - ) -> Result>> { - log::trace!("Handling streamed completion event: {:?}", event); - use LanguageModelCompletionEvent::*; - - match event { - StartMessage { .. } => { - self.flush_pending_message(cx); - self.pending_message = Some(AgentMessage::default()); - } - Text(new_text) => self.handle_text_event(new_text, event_stream, cx), - Thinking { text, signature } => { - self.handle_thinking_event(text, signature, event_stream, cx) - } - RedactedThinking { data } => self.handle_redacted_thinking_event(data, cx), - ToolUse(tool_use) => { - return Ok(self.handle_tool_use_event(tool_use, event_stream, cx)); - } - ToolUseJsonParseError { - id, - tool_name, - raw_input, - json_parse_error, - } => { - return Ok(Some(Task::ready( - self.handle_tool_use_json_parse_error_event( - id, - tool_name, - raw_input, - json_parse_error, - ), - ))); - } - UsageUpdate(usage) => { - telemetry::event!( - "Agent Thread Completion Usage Updated", - thread_id = self.id.to_string(), - prompt_id = self.prompt_id.to_string(), - model = self.model.as_ref().map(|m| m.telemetry_id()), - model_provider = self.model.as_ref().map(|m| m.provider_id().to_string()), - input_tokens = usage.input_tokens, - output_tokens = usage.output_tokens, - cache_creation_input_tokens = usage.cache_creation_input_tokens, - cache_read_input_tokens = usage.cache_read_input_tokens, - ); - self.update_token_usage(usage, cx); - } - StatusUpdate(CompletionRequestStatus::UsageUpdated { amount, limit }) => { - self.update_model_request_usage(amount, limit, cx); - } - StatusUpdate( - CompletionRequestStatus::Started - | CompletionRequestStatus::Queued { .. } - | CompletionRequestStatus::Failed { .. }, - ) => {} - StatusUpdate(CompletionRequestStatus::ToolUseLimitReached) => { - self.tool_use_limit_reached = true; - } - Stop(StopReason::Refusal) => return Err(CompletionError::Refusal.into()), - Stop(StopReason::MaxTokens) => return Err(CompletionError::MaxTokens.into()), - Stop(StopReason::ToolUse | StopReason::EndTurn) => {} - } - - Ok(None) - } - - fn handle_text_event( - &mut self, - new_text: String, - event_stream: &ThreadEventStream, - cx: &mut Context, - ) { - event_stream.send_text(&new_text); - - let last_message = self.pending_message(); - if let Some(AgentMessageContent::Text(text)) = last_message.content.last_mut() { - text.push_str(&new_text); - } else { - last_message - .content - .push(AgentMessageContent::Text(new_text)); - } - - cx.notify(); - } - - fn handle_thinking_event( - &mut self, - new_text: String, - new_signature: Option, - event_stream: &ThreadEventStream, - cx: &mut Context, - ) { - event_stream.send_thinking(&new_text); - - let last_message = self.pending_message(); - if let Some(AgentMessageContent::Thinking { text, signature }) = - last_message.content.last_mut() - { - text.push_str(&new_text); - *signature = new_signature.or(signature.take()); - } else { - last_message.content.push(AgentMessageContent::Thinking { - text: new_text, - signature: new_signature, - }); - } - - cx.notify(); - } - - fn handle_redacted_thinking_event(&mut self, data: String, cx: &mut Context) { - let last_message = self.pending_message(); - last_message - .content - .push(AgentMessageContent::RedactedThinking(data)); - cx.notify(); - } - - fn handle_tool_use_event( - &mut self, - tool_use: LanguageModelToolUse, - event_stream: &ThreadEventStream, - cx: &mut Context, - ) -> Option> { - cx.notify(); - - let tool = self.tool(tool_use.name.as_ref()); - let mut title = SharedString::from(&tool_use.name); - let mut kind = acp::ToolKind::Other; - if let Some(tool) = tool.as_ref() { - title = tool.initial_title(tool_use.input.clone()); - kind = tool.kind(); - } - - // Ensure the last message ends in the current tool use - let last_message = self.pending_message(); - let push_new_tool_use = last_message.content.last_mut().is_none_or(|content| { - if let AgentMessageContent::ToolUse(last_tool_use) = content { - if last_tool_use.id == tool_use.id { - *last_tool_use = tool_use.clone(); - false - } else { - true - } - } else { - true - } - }); - - if push_new_tool_use { - event_stream.send_tool_call(&tool_use.id, title, kind, tool_use.input.clone()); - last_message - .content - .push(AgentMessageContent::ToolUse(tool_use.clone())); - } else { - event_stream.update_tool_call_fields( - &tool_use.id, - acp::ToolCallUpdateFields { - title: Some(title.into()), - kind: Some(kind), - raw_input: Some(tool_use.input.clone()), - ..Default::default() - }, - ); - } - - if !tool_use.is_input_complete { - return None; - } - - let Some(tool) = tool else { - let content = format!("No tool named {} exists", tool_use.name); - return Some(Task::ready(LanguageModelToolResult { - content: LanguageModelToolResultContent::Text(Arc::from(content)), - tool_use_id: tool_use.id, - tool_name: tool_use.name, - is_error: true, - output: None, - })); - }; - - let fs = self.project.read(cx).fs().clone(); - let tool_event_stream = - ToolCallEventStream::new(tool_use.id.clone(), event_stream.clone(), Some(fs)); - tool_event_stream.update_fields(acp::ToolCallUpdateFields { - status: Some(acp::ToolCallStatus::InProgress), - ..Default::default() - }); - let supports_images = self.model().is_some_and(|model| model.supports_images()); - let tool_result = tool.run(tool_use.input, tool_event_stream, cx); - log::debug!("Running tool {}", tool_use.name); - Some(cx.foreground_executor().spawn(async move { - let tool_result = tool_result.await.and_then(|output| { - if let LanguageModelToolResultContent::Image(_) = &output.llm_output - && !supports_images - { - return Err(anyhow!( - "Attempted to read an image, but this model doesn't support it.", - )); - } - Ok(output) - }); - - match tool_result { - Ok(output) => LanguageModelToolResult { - tool_use_id: tool_use.id, - tool_name: tool_use.name, - is_error: false, - content: output.llm_output, - output: Some(output.raw_output), - }, - Err(error) => LanguageModelToolResult { - tool_use_id: tool_use.id, - tool_name: tool_use.name, - is_error: true, - content: LanguageModelToolResultContent::Text(Arc::from(error.to_string())), - output: Some(error.to_string().into()), - }, - } - })) - } - - fn handle_tool_use_json_parse_error_event( - &mut self, - tool_use_id: LanguageModelToolUseId, - tool_name: Arc, - raw_input: Arc, - json_parse_error: String, - ) -> LanguageModelToolResult { - let tool_output = format!("Error parsing input JSON: {json_parse_error}"); - LanguageModelToolResult { - tool_use_id, - tool_name, - is_error: true, - content: LanguageModelToolResultContent::Text(tool_output.into()), - output: Some(serde_json::Value::String(raw_input.to_string())), - } - } - - fn update_model_request_usage(&self, amount: usize, limit: UsageLimit, cx: &mut Context) { - self.project - .read(cx) - .user_store() - .update(cx, |user_store, cx| { - user_store.update_model_request_usage( - ModelRequestUsage(RequestUsage { - amount: amount as i32, - limit, - }), - cx, - ) - }); - } - - pub fn title(&self) -> SharedString { - self.title.clone().unwrap_or("New Thread".into()) - } - - pub fn summary(&mut self, cx: &mut Context) -> Task> { - if let Some(summary) = self.summary.as_ref() { - return Task::ready(Ok(summary.clone())); - } - let Some(model) = self.summarization_model.clone() else { - return Task::ready(Err(anyhow!("No summarization model available"))); - }; - let mut request = LanguageModelRequest { - intent: Some(CompletionIntent::ThreadContextSummarization), - temperature: AgentSettings::temperature_for_model(&model, cx), - ..Default::default() - }; - - for message in &self.messages { - request.messages.extend(message.to_request()); - } - - request.messages.push(LanguageModelRequestMessage { - role: Role::User, - content: vec![SUMMARIZE_THREAD_DETAILED_PROMPT.into()], - cache: false, - }); - cx.spawn(async move |this, cx| { - let mut summary = String::new(); - let mut messages = model.stream_completion(request, cx).await?; - while let Some(event) = messages.next().await { - let event = event?; - let text = match event { - LanguageModelCompletionEvent::Text(text) => text, - LanguageModelCompletionEvent::StatusUpdate( - CompletionRequestStatus::UsageUpdated { amount, limit }, - ) => { - this.update(cx, |thread, cx| { - thread.update_model_request_usage(amount, limit, cx); - })?; - continue; - } - _ => continue, - }; - - let mut lines = text.lines(); - summary.extend(lines.next()); - } - - log::debug!("Setting summary: {}", summary); - let summary = SharedString::from(summary); - - this.update(cx, |this, cx| { - this.summary = Some(summary.clone()); - cx.notify() - })?; - - Ok(summary) - }) - } - - fn generate_title(&mut self, cx: &mut Context) { - let Some(model) = self.summarization_model.clone() else { - return; - }; - - log::debug!( - "Generating title with model: {:?}", - self.summarization_model.as_ref().map(|model| model.name()) - ); - let mut request = LanguageModelRequest { - intent: Some(CompletionIntent::ThreadSummarization), - temperature: AgentSettings::temperature_for_model(&model, cx), - ..Default::default() - }; - - for message in &self.messages { - request.messages.extend(message.to_request()); - } - - request.messages.push(LanguageModelRequestMessage { - role: Role::User, - content: vec![SUMMARIZE_THREAD_PROMPT.into()], - cache: false, - }); - self.pending_title_generation = Some(cx.spawn(async move |this, cx| { - let mut title = String::new(); - - let generate = async { - let mut messages = model.stream_completion(request, cx).await?; - while let Some(event) = messages.next().await { - let event = event?; - let text = match event { - LanguageModelCompletionEvent::Text(text) => text, - LanguageModelCompletionEvent::StatusUpdate( - CompletionRequestStatus::UsageUpdated { amount, limit }, - ) => { - this.update(cx, |thread, cx| { - thread.update_model_request_usage(amount, limit, cx); - })?; - continue; - } - _ => continue, - }; - - let mut lines = text.lines(); - title.extend(lines.next()); - - // Stop if the LLM generated multiple lines. - if lines.next().is_some() { - break; - } - } - anyhow::Ok(()) - }; - - if generate.await.context("failed to generate title").is_ok() { - _ = this.update(cx, |this, cx| this.set_title(title.into(), cx)); - } - _ = this.update(cx, |this, _| this.pending_title_generation = None); - })); - } - - pub fn set_title(&mut self, title: SharedString, cx: &mut Context) { - self.pending_title_generation = None; - if Some(&title) != self.title.as_ref() { - self.title = Some(title); - cx.emit(TitleUpdated); - cx.notify(); - } - } - - fn last_user_message(&self) -> Option<&UserMessage> { - self.messages - .iter() - .rev() - .find_map(|message| match message { - Message::User(user_message) => Some(user_message), - Message::Agent(_) => None, - Message::Resume => None, - }) - } - - fn pending_message(&mut self) -> &mut AgentMessage { - self.pending_message.get_or_insert_default() - } - - fn flush_pending_message(&mut self, cx: &mut Context) { - let Some(mut message) = self.pending_message.take() else { - return; - }; - - if message.content.is_empty() { - return; - } - - for content in &message.content { - let AgentMessageContent::ToolUse(tool_use) = content else { - continue; - }; - - if !message.tool_results.contains_key(&tool_use.id) { - message.tool_results.insert( - tool_use.id.clone(), - LanguageModelToolResult { - tool_use_id: tool_use.id.clone(), - tool_name: tool_use.name.clone(), - is_error: true, - content: LanguageModelToolResultContent::Text(TOOL_CANCELED_MESSAGE.into()), - output: None, - }, - ); - } - } - - self.messages.push(Message::Agent(message)); - self.updated_at = Utc::now(); - self.summary = None; - cx.notify() - } - - pub(crate) fn build_completion_request( - &self, - completion_intent: CompletionIntent, - cx: &App, - ) -> Result { - let model = self.model().context("No language model configured")?; - let tools = if let Some(turn) = self.running_turn.as_ref() { - turn.tools - .iter() - .filter_map(|(tool_name, tool)| { - log::trace!("Including tool: {}", tool_name); - Some(LanguageModelRequestTool { - name: tool_name.to_string(), - description: tool.description().to_string(), - input_schema: tool.input_schema(model.tool_input_format()).log_err()?, - }) - }) - .collect::>() - } else { - Vec::new() - }; - - log::debug!("Building completion request"); - log::debug!("Completion intent: {:?}", completion_intent); - log::debug!("Completion mode: {:?}", self.completion_mode); - - let messages = self.build_request_messages(cx); - log::debug!("Request will include {} messages", messages.len()); - log::debug!("Request includes {} tools", tools.len()); - - let request = LanguageModelRequest { - thread_id: Some(self.id.to_string()), - prompt_id: Some(self.prompt_id.to_string()), - intent: Some(completion_intent), - mode: Some(self.completion_mode.into()), - messages, - tools, - tool_choice: None, - stop: Vec::new(), - temperature: AgentSettings::temperature_for_model(model, cx), - thinking_allowed: true, - }; - - log::debug!("Completion request built successfully"); - Ok(request) - } - - fn enabled_tools( - &self, - profile: &AgentProfileSettings, - model: &Arc, - cx: &App, - ) -> BTreeMap> { - fn truncate(tool_name: &SharedString) -> SharedString { - if tool_name.len() > MAX_TOOL_NAME_LENGTH { - let mut truncated = tool_name.to_string(); - truncated.truncate(MAX_TOOL_NAME_LENGTH); - truncated.into() - } else { - tool_name.clone() - } - } - - let mut tools = self - .tools - .iter() - .filter_map(|(tool_name, tool)| { - if tool.supported_provider(&model.provider_id()) - && profile.is_tool_enabled(tool_name) - { - Some((truncate(tool_name), tool.clone())) - } else { - None - } - }) - .collect::>(); - - let mut context_server_tools = Vec::new(); - let mut seen_tools = tools.keys().cloned().collect::>(); - let mut duplicate_tool_names = HashSet::default(); - for (server_id, server_tools) in self.context_server_registry.read(cx).servers() { - for (tool_name, tool) in server_tools { - if profile.is_context_server_tool_enabled(&server_id.0, &tool_name) { - let tool_name = truncate(tool_name); - if !seen_tools.insert(tool_name.clone()) { - duplicate_tool_names.insert(tool_name.clone()); - } - context_server_tools.push((server_id.clone(), tool_name, tool.clone())); - } - } - } - - // When there are duplicate tool names, disambiguate by prefixing them - // with the server ID. In the rare case there isn't enough space for the - // disambiguated tool name, keep only the last tool with this name. - for (server_id, tool_name, tool) in context_server_tools { - if duplicate_tool_names.contains(&tool_name) { - let available = MAX_TOOL_NAME_LENGTH.saturating_sub(tool_name.len()); - if available >= 2 { - let mut disambiguated = server_id.0.to_string(); - disambiguated.truncate(available - 1); - disambiguated.push('_'); - disambiguated.push_str(&tool_name); - tools.insert(disambiguated.into(), tool.clone()); - } else { - tools.insert(tool_name, tool.clone()); - } - } else { - tools.insert(tool_name, tool.clone()); - } - } - - tools - } - - fn tool(&self, name: &str) -> Option> { - self.running_turn.as_ref()?.tools.get(name).cloned() - } - - fn build_request_messages(&self, cx: &App) -> Vec { - log::trace!( - "Building request messages from {} thread messages", - self.messages.len() - ); - - let system_prompt = SystemPromptTemplate { - project: self.project_context.read(cx), - available_tools: self.tools.keys().cloned().collect(), - } - .render(&self.templates) - .context("failed to build system prompt") - .expect("Invalid template"); - let mut messages = vec![LanguageModelRequestMessage { - role: Role::System, - content: vec![system_prompt.into()], - cache: false, - }]; - for message in &self.messages { - messages.extend(message.to_request()); - } - - if let Some(last_message) = messages.last_mut() { - last_message.cache = true; - } - - if let Some(message) = self.pending_message.as_ref() { - messages.extend(message.to_request()); - } - - messages - } - - pub fn to_markdown(&self) -> String { - let mut markdown = String::new(); - for (ix, message) in self.messages.iter().enumerate() { - if ix > 0 { - markdown.push('\n'); - } - markdown.push_str(&message.to_markdown()); - } - - if let Some(message) = self.pending_message.as_ref() { - markdown.push('\n'); - markdown.push_str(&message.to_markdown()); - } - - markdown - } - - fn advance_prompt_id(&mut self) { - self.prompt_id = PromptId::new(); - } - - fn retry_strategy_for(error: &LanguageModelCompletionError) -> Option { - use LanguageModelCompletionError::*; - use http_client::StatusCode; - - // General strategy here: - // - If retrying won't help (e.g. invalid API key or payload too large), return None so we don't retry at all. - // - If it's a time-based issue (e.g. server overloaded, rate limit exceeded), retry up to 4 times with exponential backoff. - // - If it's an issue that *might* be fixed by retrying (e.g. internal server error), retry up to 3 times. - match error { - HttpResponseError { - status_code: StatusCode::TOO_MANY_REQUESTS, - .. - } => Some(RetryStrategy::ExponentialBackoff { - initial_delay: BASE_RETRY_DELAY, - max_attempts: MAX_RETRY_ATTEMPTS, - }), - ServerOverloaded { retry_after, .. } | RateLimitExceeded { retry_after, .. } => { - Some(RetryStrategy::Fixed { - delay: retry_after.unwrap_or(BASE_RETRY_DELAY), - max_attempts: MAX_RETRY_ATTEMPTS, - }) - } - UpstreamProviderError { - status, - retry_after, - .. - } => match *status { - StatusCode::TOO_MANY_REQUESTS | StatusCode::SERVICE_UNAVAILABLE => { - Some(RetryStrategy::Fixed { - delay: retry_after.unwrap_or(BASE_RETRY_DELAY), - max_attempts: MAX_RETRY_ATTEMPTS, - }) - } - StatusCode::INTERNAL_SERVER_ERROR => Some(RetryStrategy::Fixed { - delay: retry_after.unwrap_or(BASE_RETRY_DELAY), - // Internal Server Error could be anything, retry up to 3 times. - max_attempts: 3, - }), - status => { - // There is no StatusCode variant for the unofficial HTTP 529 ("The service is overloaded"), - // but we frequently get them in practice. See https://http.dev/529 - if status.as_u16() == 529 { - Some(RetryStrategy::Fixed { - delay: retry_after.unwrap_or(BASE_RETRY_DELAY), - max_attempts: MAX_RETRY_ATTEMPTS, - }) - } else { - Some(RetryStrategy::Fixed { - delay: retry_after.unwrap_or(BASE_RETRY_DELAY), - max_attempts: 2, - }) - } - } - }, - ApiInternalServerError { .. } => Some(RetryStrategy::Fixed { - delay: BASE_RETRY_DELAY, - max_attempts: 3, - }), - ApiReadResponseError { .. } - | HttpSend { .. } - | DeserializeResponse { .. } - | BadRequestFormat { .. } => Some(RetryStrategy::Fixed { - delay: BASE_RETRY_DELAY, - max_attempts: 3, - }), - // Retrying these errors definitely shouldn't help. - HttpResponseError { - status_code: - StatusCode::PAYLOAD_TOO_LARGE | StatusCode::FORBIDDEN | StatusCode::UNAUTHORIZED, - .. - } - | AuthenticationError { .. } - | PermissionError { .. } - | NoApiKey { .. } - | ApiEndpointNotFound { .. } - | PromptTooLarge { .. } => None, - // These errors might be transient, so retry them - SerializeRequest { .. } | BuildRequestBody { .. } => Some(RetryStrategy::Fixed { - delay: BASE_RETRY_DELAY, - max_attempts: 1, - }), - // Retry all other 4xx and 5xx errors once. - HttpResponseError { status_code, .. } - if status_code.is_client_error() || status_code.is_server_error() => - { - Some(RetryStrategy::Fixed { - delay: BASE_RETRY_DELAY, - max_attempts: 3, - }) - } - Other(err) - if err.is::() - || err.is::() => - { - // Retrying won't help for Payment Required or Model Request Limit errors (where - // the user must upgrade to usage-based billing to get more requests, or else wait - // for a significant amount of time for the request limit to reset). - None - } - // Conservatively assume that any other errors are non-retryable - HttpResponseError { .. } | Other(..) => Some(RetryStrategy::Fixed { - delay: BASE_RETRY_DELAY, - max_attempts: 2, - }), - } - } -} - -struct RunningTurn { - /// 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. - _task: Task<()>, - /// The current event stream for the running turn. Used to report a final - /// cancellation event if we cancel the turn. - event_stream: ThreadEventStream, - /// The tools that were enabled for this turn. - tools: BTreeMap>, -} - -impl RunningTurn { - fn cancel(self) { - log::debug!("Cancelling in progress turn"); - self.event_stream.send_canceled(); - } -} - -pub struct TokenUsageUpdated(pub Option); - -impl EventEmitter for Thread {} - -pub struct TitleUpdated; - -impl EventEmitter for Thread {} - -pub trait AgentTool -where - Self: 'static + Sized, -{ - type Input: for<'de> Deserialize<'de> + Serialize + JsonSchema; - type Output: for<'de> Deserialize<'de> + Serialize + Into; - - fn name() -> &'static str; - - fn description(&self) -> SharedString { - let schema = schemars::schema_for!(Self::Input); - SharedString::new( - schema - .get("description") - .and_then(|description| description.as_str()) - .unwrap_or_default(), - ) - } - - fn kind() -> acp::ToolKind; - - /// The initial tool title to display. Can be updated during the tool run. - fn initial_title(&self, input: Result) -> SharedString; - - /// Returns the JSON schema that describes the tool's input. - fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Schema { - crate::tool_schema::root_schema_for::(format) - } - - /// Some tools rely on a provider for the underlying billing or other reasons. - /// Allow the tool to check if they are compatible, or should be filtered out. - fn supported_provider(&self, _provider: &LanguageModelProviderId) -> bool { - true - } - - /// Runs the tool with the provided input. - fn run( - self: Arc, - input: Self::Input, - event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task>; - - /// Emits events for a previous execution of the tool. - fn replay( - &self, - _input: Self::Input, - _output: Self::Output, - _event_stream: ToolCallEventStream, - _cx: &mut App, - ) -> Result<()> { - Ok(()) - } - - fn erase(self) -> Arc { - Arc::new(Erased(Arc::new(self))) - } -} - -pub struct Erased(T); - -pub struct AgentToolOutput { - pub llm_output: LanguageModelToolResultContent, - pub raw_output: serde_json::Value, -} - -pub trait AnyAgentTool { - fn name(&self) -> SharedString; - fn description(&self) -> SharedString; - fn kind(&self) -> acp::ToolKind; - fn initial_title(&self, input: serde_json::Value) -> SharedString; - fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result; - fn supported_provider(&self, _provider: &LanguageModelProviderId) -> bool { - true - } - fn run( - self: Arc, - input: serde_json::Value, - event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task>; - fn replay( - &self, - input: serde_json::Value, - output: serde_json::Value, - event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Result<()>; -} - -impl AnyAgentTool for Erased> -where - T: AgentTool, -{ - fn name(&self) -> SharedString { - T::name().into() - } - - fn description(&self) -> SharedString { - self.0.description() - } - - fn kind(&self) -> agent_client_protocol::ToolKind { - T::kind() - } - - fn initial_title(&self, input: serde_json::Value) -> SharedString { - let parsed_input = serde_json::from_value(input.clone()).map_err(|_| input); - self.0.initial_title(parsed_input) - } - - fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { - let mut json = serde_json::to_value(self.0.input_schema(format))?; - adapt_schema_to_format(&mut json, format)?; - Ok(json) - } - - fn supported_provider(&self, provider: &LanguageModelProviderId) -> bool { - self.0.supported_provider(provider) - } - - fn run( - self: Arc, - input: serde_json::Value, - event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task> { - cx.spawn(async move |cx| { - let input = serde_json::from_value(input)?; - let output = cx - .update(|cx| self.0.clone().run(input, event_stream, cx))? - .await?; - let raw_output = serde_json::to_value(&output)?; - Ok(AgentToolOutput { - llm_output: output.into(), - raw_output, - }) - }) - } - - fn replay( - &self, - input: serde_json::Value, - output: serde_json::Value, - event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Result<()> { - let input = serde_json::from_value(input)?; - let output = serde_json::from_value(output)?; - self.0.replay(input, output, event_stream, cx) - } -} - -#[derive(Clone)] -struct ThreadEventStream(mpsc::UnboundedSender>); - -impl ThreadEventStream { - fn send_user_message(&self, message: &UserMessage) { - self.0 - .unbounded_send(Ok(ThreadEvent::UserMessage(message.clone()))) - .ok(); - } - - fn send_text(&self, text: &str) { - self.0 - .unbounded_send(Ok(ThreadEvent::AgentText(text.to_string()))) - .ok(); - } - - fn send_thinking(&self, text: &str) { - self.0 - .unbounded_send(Ok(ThreadEvent::AgentThinking(text.to_string()))) - .ok(); - } - - fn send_tool_call( - &self, - id: &LanguageModelToolUseId, - title: SharedString, - kind: acp::ToolKind, - input: serde_json::Value, - ) { - self.0 - .unbounded_send(Ok(ThreadEvent::ToolCall(Self::initial_tool_call( - id, - title.to_string(), - kind, - input, - )))) - .ok(); - } - - fn initial_tool_call( - id: &LanguageModelToolUseId, - title: String, - kind: acp::ToolKind, - input: serde_json::Value, - ) -> acp::ToolCall { - acp::ToolCall { - id: acp::ToolCallId(id.to_string().into()), - title, - kind, - status: acp::ToolCallStatus::Pending, - content: vec![], - locations: vec![], - raw_input: Some(input), - raw_output: None, - } - } - - fn update_tool_call_fields( - &self, - tool_use_id: &LanguageModelToolUseId, - fields: acp::ToolCallUpdateFields, - ) { - self.0 - .unbounded_send(Ok(ThreadEvent::ToolCallUpdate( - acp::ToolCallUpdate { - id: acp::ToolCallId(tool_use_id.to_string().into()), - fields, - } - .into(), - ))) - .ok(); - } - - fn send_retry(&self, status: acp_thread::RetryStatus) { - self.0.unbounded_send(Ok(ThreadEvent::Retry(status))).ok(); - } - - fn send_stop(&self, reason: acp::StopReason) { - self.0.unbounded_send(Ok(ThreadEvent::Stop(reason))).ok(); - } - - fn send_canceled(&self) { - self.0 - .unbounded_send(Ok(ThreadEvent::Stop(acp::StopReason::Cancelled))) - .ok(); - } - - fn send_error(&self, error: impl Into) { - self.0.unbounded_send(Err(error.into())).ok(); - } -} - -#[derive(Clone)] -pub struct ToolCallEventStream { - tool_use_id: LanguageModelToolUseId, - stream: ThreadEventStream, - fs: Option>, -} - -impl ToolCallEventStream { - #[cfg(test)] - pub fn test() -> (Self, ToolCallEventStreamReceiver) { - let (events_tx, events_rx) = mpsc::unbounded::>(); - - let stream = ToolCallEventStream::new("test_id".into(), ThreadEventStream(events_tx), None); - - (stream, ToolCallEventStreamReceiver(events_rx)) - } - - fn new( - tool_use_id: LanguageModelToolUseId, - stream: ThreadEventStream, - fs: Option>, - ) -> Self { - Self { - tool_use_id, - stream, - fs, - } - } - - pub fn update_fields(&self, fields: acp::ToolCallUpdateFields) { - self.stream - .update_tool_call_fields(&self.tool_use_id, fields); - } - - pub fn update_diff(&self, diff: Entity) { - self.stream - .0 - .unbounded_send(Ok(ThreadEvent::ToolCallUpdate( - acp_thread::ToolCallUpdateDiff { - id: acp::ToolCallId(self.tool_use_id.to_string().into()), - diff, - } - .into(), - ))) - .ok(); - } - - pub fn update_terminal(&self, terminal: Entity) { - self.stream - .0 - .unbounded_send(Ok(ThreadEvent::ToolCallUpdate( - acp_thread::ToolCallUpdateTerminal { - id: acp::ToolCallId(self.tool_use_id.to_string().into()), - terminal, - } - .into(), - ))) - .ok(); - } - - pub fn authorize(&self, title: impl Into, cx: &mut App) -> Task> { - if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions { - return Task::ready(Ok(())); - } - - let (response_tx, response_rx) = oneshot::channel(); - self.stream - .0 - .unbounded_send(Ok(ThreadEvent::ToolCallAuthorization( - ToolCallAuthorization { - tool_call: acp::ToolCallUpdate { - id: acp::ToolCallId(self.tool_use_id.to_string().into()), - fields: acp::ToolCallUpdateFields { - title: Some(title.into()), - ..Default::default() - }, - }, - options: vec![ - acp::PermissionOption { - id: acp::PermissionOptionId("always_allow".into()), - name: "Always Allow".into(), - kind: acp::PermissionOptionKind::AllowAlways, - }, - acp::PermissionOption { - id: acp::PermissionOptionId("allow".into()), - name: "Allow".into(), - kind: acp::PermissionOptionKind::AllowOnce, - }, - acp::PermissionOption { - id: acp::PermissionOptionId("deny".into()), - name: "Deny".into(), - kind: acp::PermissionOptionKind::RejectOnce, - }, - ], - response: response_tx, - }, - ))) - .ok(); - let fs = self.fs.clone(); - cx.spawn(async move |cx| match response_rx.await?.0.as_ref() { - "always_allow" => { - if let Some(fs) = fs.clone() { - cx.update(|cx| { - update_settings_file::(fs, cx, |settings, _| { - settings.set_always_allow_tool_actions(true); - }); - })?; - } - - Ok(()) - } - "allow" => Ok(()), - _ => Err(anyhow!("Permission to run tool denied by user")), - }) - } -} - -#[cfg(test)] -pub struct ToolCallEventStreamReceiver(mpsc::UnboundedReceiver>); - -#[cfg(test)] -impl ToolCallEventStreamReceiver { - pub async fn expect_authorization(&mut self) -> ToolCallAuthorization { - let event = self.0.next().await; - if let Some(Ok(ThreadEvent::ToolCallAuthorization(auth))) = event { - auth - } else { - panic!("Expected ToolCallAuthorization but got: {:?}", event); - } - } - - pub async fn expect_update_fields(&mut self) -> acp::ToolCallUpdateFields { - let event = self.0.next().await; - if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields( - update, - )))) = event - { - update.fields - } else { - panic!("Expected update fields but got: {:?}", event); - } - } - - pub async fn expect_diff(&mut self) -> Entity { - let event = self.0.next().await; - if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateDiff( - update, - )))) = event - { - update.diff - } else { - panic!("Expected diff but got: {:?}", event); - } - } - - pub async fn expect_terminal(&mut self) -> Entity { - let event = self.0.next().await; - if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateTerminal( - update, - )))) = event - { - update.terminal - } else { - panic!("Expected terminal but got: {:?}", event); - } - } -} - -#[cfg(test)] -impl std::ops::Deref for ToolCallEventStreamReceiver { - type Target = mpsc::UnboundedReceiver>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -#[cfg(test)] -impl std::ops::DerefMut for ToolCallEventStreamReceiver { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl From<&str> for UserMessageContent { - fn from(text: &str) -> Self { - Self::Text(text.into()) - } -} - -impl From for UserMessageContent { - fn from(value: acp::ContentBlock) -> Self { - match value { - acp::ContentBlock::Text(text_content) => Self::Text(text_content.text), - acp::ContentBlock::Image(image_content) => Self::Image(convert_image(image_content)), - acp::ContentBlock::Audio(_) => { - // TODO - Self::Text("[audio]".to_string()) - } - acp::ContentBlock::ResourceLink(resource_link) => { - match MentionUri::parse(&resource_link.uri) { - Ok(uri) => Self::Mention { - uri, - content: String::new(), - }, - Err(err) => { - log::error!("Failed to parse mention link: {}", err); - Self::Text(format!("[{}]({})", resource_link.name, resource_link.uri)) - } - } - } - acp::ContentBlock::Resource(resource) => match resource.resource { - acp::EmbeddedResourceResource::TextResourceContents(resource) => { - match MentionUri::parse(&resource.uri) { - Ok(uri) => Self::Mention { - uri, - content: resource.text, - }, - Err(err) => { - log::error!("Failed to parse mention link: {}", err); - Self::Text( - MarkdownCodeBlock { - tag: &resource.uri, - text: &resource.text, - } - .to_string(), - ) - } - } - } - acp::EmbeddedResourceResource::BlobResourceContents(_) => { - // TODO - Self::Text("[blob]".to_string()) - } - }, - } - } -} - -impl From for acp::ContentBlock { - fn from(content: UserMessageContent) -> Self { - match content { - UserMessageContent::Text(text) => acp::ContentBlock::Text(acp::TextContent { - text, - annotations: None, - }), - UserMessageContent::Image(image) => acp::ContentBlock::Image(acp::ImageContent { - data: image.source.to_string(), - mime_type: "image/png".to_string(), - annotations: None, - uri: None, - }), - UserMessageContent::Mention { uri, content } => { - acp::ContentBlock::Resource(acp::EmbeddedResource { - resource: acp::EmbeddedResourceResource::TextResourceContents( - acp::TextResourceContents { - mime_type: None, - text: content, - uri: uri.to_uri().to_string(), - }, - ), - annotations: None, - }) - } - } - } -} - -fn convert_image(image_content: acp::ImageContent) -> LanguageModelImage { - LanguageModelImage { - source: image_content.data.into(), - // TODO: make this optional? - size: gpui::Size::new(0.into(), 0.into()), - } -} diff --git a/crates/agent2/src/tool_schema.rs b/crates/agent2/src/tool_schema.rs deleted file mode 100644 index f608336b41..0000000000 --- a/crates/agent2/src/tool_schema.rs +++ /dev/null @@ -1,43 +0,0 @@ -use language_model::LanguageModelToolSchemaFormat; -use schemars::{ - JsonSchema, Schema, - generate::SchemaSettings, - transform::{Transform, transform_subschemas}, -}; - -pub(crate) fn root_schema_for(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::() -} - -#[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, 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); - } -} diff --git a/crates/agent2/src/tools.rs b/crates/agent2/src/tools.rs deleted file mode 100644 index bcca7eecd1..0000000000 --- a/crates/agent2/src/tools.rs +++ /dev/null @@ -1,60 +0,0 @@ -mod context_server_registry; -mod copy_path_tool; -mod create_directory_tool; -mod delete_path_tool; -mod diagnostics_tool; -mod edit_file_tool; -mod fetch_tool; -mod find_path_tool; -mod grep_tool; -mod list_directory_tool; -mod move_path_tool; -mod now_tool; -mod open_tool; -mod read_file_tool; -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 { - [ - 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::*; -pub use delete_path_tool::*; -pub use diagnostics_tool::*; -pub use edit_file_tool::*; -pub use fetch_tool::*; -pub use find_path_tool::*; -pub use grep_tool::*; -pub use list_directory_tool::*; -pub use move_path_tool::*; -pub use now_tool::*; -pub use open_tool::*; -pub use read_file_tool::*; -pub use terminal_tool::*; -pub use thinking_tool::*; -pub use web_search_tool::*; - -use crate::AgentTool; diff --git a/crates/agent2/src/tools/context_server_registry.rs b/crates/agent2/src/tools/context_server_registry.rs deleted file mode 100644 index c7963fa6e6..0000000000 --- a/crates/agent2/src/tools/context_server_registry.rs +++ /dev/null @@ -1,239 +0,0 @@ -use crate::{AgentToolOutput, AnyAgentTool, ToolCallEventStream}; -use agent_client_protocol::ToolKind; -use anyhow::{Result, anyhow, bail}; -use collections::{BTreeMap, HashMap}; -use context_server::ContextServerId; -use gpui::{App, Context, Entity, SharedString, Task}; -use project::context_server_store::{ContextServerStatus, ContextServerStore}; -use std::sync::Arc; -use util::ResultExt; - -pub struct ContextServerRegistry { - server_store: Entity, - registered_servers: HashMap, - _subscription: gpui::Subscription, -} - -struct RegisteredContextServer { - tools: BTreeMap>, - load_tools: Task>, -} - -impl ContextServerRegistry { - pub fn new(server_store: Entity, cx: &mut Context) -> Self { - let mut this = Self { - server_store: server_store.clone(), - registered_servers: HashMap::default(), - _subscription: cx.subscribe(&server_store, Self::handle_context_server_store_event), - }; - for server in server_store.read(cx).running_servers() { - this.reload_tools_for_server(server.id(), cx); - } - this - } - - pub fn servers( - &self, - ) -> impl Iterator< - Item = ( - &ContextServerId, - &BTreeMap>, - ), - > { - self.registered_servers - .iter() - .map(|(id, server)| (id, &server.tools)) - } - - fn reload_tools_for_server(&mut self, server_id: ContextServerId, cx: &mut Context) { - let Some(server) = self.server_store.read(cx).get_running_server(&server_id) else { - return; - }; - let Some(client) = server.client() else { - return; - }; - if !client.capable(context_server::protocol::ServerCapability::Tools) { - return; - } - - let registered_server = - self.registered_servers - .entry(server_id.clone()) - .or_insert(RegisteredContextServer { - tools: BTreeMap::default(), - load_tools: Task::ready(Ok(())), - }); - registered_server.load_tools = cx.spawn(async move |this, cx| { - let response = client - .request::(()) - .await; - - this.update(cx, |this, cx| { - let Some(registered_server) = this.registered_servers.get_mut(&server_id) else { - return; - }; - - registered_server.tools.clear(); - if let Some(response) = response.log_err() { - for tool in response.tools { - let tool = Arc::new(ContextServerTool::new( - this.server_store.clone(), - server.id(), - tool, - )); - registered_server.tools.insert(tool.name(), tool); - } - cx.notify(); - } - }) - }); - } - - fn handle_context_server_store_event( - &mut self, - _: Entity, - event: &project::context_server_store::Event, - cx: &mut Context, - ) { - match event { - project::context_server_store::Event::ServerStatusChanged { server_id, status } => { - match status { - ContextServerStatus::Starting => {} - ContextServerStatus::Running => { - self.reload_tools_for_server(server_id.clone(), cx); - } - ContextServerStatus::Stopped | ContextServerStatus::Error(_) => { - self.registered_servers.remove(server_id); - cx.notify(); - } - } - } - } - } -} - -struct ContextServerTool { - store: Entity, - server_id: ContextServerId, - tool: context_server::types::Tool, -} - -impl ContextServerTool { - fn new( - store: Entity, - server_id: ContextServerId, - tool: context_server::types::Tool, - ) -> Self { - Self { - store, - server_id, - tool, - } - } -} - -impl AnyAgentTool for ContextServerTool { - fn name(&self) -> SharedString { - self.tool.name.clone().into() - } - - fn description(&self) -> SharedString { - self.tool.description.clone().unwrap_or_default().into() - } - - fn kind(&self) -> ToolKind { - ToolKind::Other - } - - fn initial_title(&self, _input: serde_json::Value) -> SharedString { - format!("Run MCP tool `{}`", self.tool.name).into() - } - - fn input_schema( - &self, - format: language_model::LanguageModelToolSchemaFormat, - ) -> Result { - let mut schema = self.tool.input_schema.clone(); - assistant_tool::adapt_schema_to_format(&mut schema, format)?; - Ok(match schema { - serde_json::Value::Null => { - serde_json::json!({ "type": "object", "properties": [] }) - } - serde_json::Value::Object(map) if map.is_empty() => { - serde_json::json!({ "type": "object", "properties": [] }) - } - _ => schema, - }) - } - - fn run( - self: Arc, - input: serde_json::Value, - _event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task> { - 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(); - - cx.spawn(async move |_cx| { - let Some(protocol) = server.client() else { - bail!("Context server not initialized"); - }; - - let arguments = if let serde_json::Value::Object(map) = input { - Some(map.into_iter().collect()) - } else { - None - }; - - log::trace!( - "Running tool: {} with arguments: {:?}", - tool_name, - arguments - ); - let response = protocol - .request::( - context_server::types::CallToolParams { - name: tool_name, - arguments, - meta: None, - }, - ) - .await?; - - let mut result = String::new(); - for content in response.content { - match content { - context_server::types::ToolResponseContent::Text { text } => { - result.push_str(&text); - } - context_server::types::ToolResponseContent::Image { .. } => { - log::warn!("Ignoring image content from tool response"); - } - context_server::types::ToolResponseContent::Audio { .. } => { - log::warn!("Ignoring audio content from tool response"); - } - context_server::types::ToolResponseContent::Resource { .. } => { - log::warn!("Ignoring resource content from tool response"); - } - } - } - Ok(AgentToolOutput { - raw_output: result.clone().into(), - llm_output: result.into(), - }) - }) - } - - fn replay( - &self, - _input: serde_json::Value, - _output: serde_json::Value, - _event_stream: ToolCallEventStream, - _cx: &mut App, - ) -> Result<()> { - Ok(()) - } -} diff --git a/crates/agent2/src/tools/copy_path_tool.rs b/crates/agent2/src/tools/copy_path_tool.rs deleted file mode 100644 index 819a6ff209..0000000000 --- a/crates/agent2/src/tools/copy_path_tool.rs +++ /dev/null @@ -1,111 +0,0 @@ -use crate::{AgentTool, ToolCallEventStream}; -use agent_client_protocol::ToolKind; -use anyhow::{Context as _, Result, anyhow}; -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. -/// 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. -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct CopyPathToolInput { - /// The source path of the file or directory to copy. - /// If a directory is specified, its contents will be copied recursively (like `cp -r`). - /// - /// - /// If the project has the following files: - /// - /// - directory1/a/something.txt - /// - directory2/a/things.txt - /// - directory3/a/other.txt - /// - /// You can copy the first file by providing a source_path of "directory1/a/something.txt" - /// - pub source_path: String, - /// The destination path where the file or directory should be copied to. - /// - /// - /// To copy "directory1/a/something.txt" to "directory2/b/copy.txt", provide a destination_path of "directory2/b/copy.txt" - /// - pub destination_path: String, -} - -pub struct CopyPathTool { - project: Entity, -} - -impl CopyPathTool { - pub fn new(project: Entity) -> Self { - Self { project } - } -} - -impl AgentTool for CopyPathTool { - type Input = CopyPathToolInput; - type Output = String; - - fn name() -> &'static str { - "copy_path" - } - - fn kind() -> ToolKind { - ToolKind::Move - } - - fn initial_title(&self, input: Result) -> ui::SharedString { - if let Ok(input) = input { - let src = MarkdownInlineCode(&input.source_path); - let dest = MarkdownInlineCode(&input.destination_path); - format!("Copy {src} to {dest}").into() - } else { - "Copy path".into() - } - } - - fn run( - self: Arc, - input: Self::Input, - _event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task> { - let copy_task = self.project.update(cx, |project, cx| { - match project - .find_project_path(&input.source_path, cx) - .and_then(|project_path| project.entry_for_path(&project_path, cx)) - { - Some(entity) => match project.find_project_path(&input.destination_path, cx) { - Some(project_path) => { - project.copy_entry(entity.id, None, project_path.path, cx) - } - None => Task::ready(Err(anyhow!( - "Destination path {} was outside the project.", - input.destination_path - ))), - }, - None => Task::ready(Err(anyhow!( - "Source path {} was not found in the project.", - input.source_path - ))), - } - }); - - cx.background_spawn(async move { - let _ = copy_task.await.with_context(|| { - format!( - "Copying {} to {}", - input.source_path, input.destination_path - ) - })?; - Ok(format!( - "Copied {} to {}", - input.source_path, input.destination_path - )) - }) - } -} diff --git a/crates/agent2/src/tools/create_directory_tool.rs b/crates/agent2/src/tools/create_directory_tool.rs deleted file mode 100644 index 652363d5fa..0000000000 --- a/crates/agent2/src/tools/create_directory_tool.rs +++ /dev/null @@ -1,86 +0,0 @@ -use agent_client_protocol::ToolKind; -use anyhow::{Context as _, Result, anyhow}; -use gpui::{App, Entity, SharedString, Task}; -use project::Project; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::sync::Arc; -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. -/// -/// 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. - /// - /// - /// If the project has the following structure: - /// - /// - directory1/ - /// - directory2/ - /// - /// You can create a new directory by providing a path of "directory1/new_directory" - /// - pub path: String, -} - -pub struct CreateDirectoryTool { - project: Entity, -} - -impl CreateDirectoryTool { - pub fn new(project: Entity) -> Self { - Self { project } - } -} - -impl AgentTool for CreateDirectoryTool { - type Input = CreateDirectoryToolInput; - type Output = String; - - fn name() -> &'static str { - "create_directory" - } - - fn kind() -> ToolKind { - ToolKind::Read - } - - fn initial_title(&self, input: Result) -> SharedString { - if let Ok(input) = input { - format!("Create directory {}", MarkdownInlineCode(&input.path)).into() - } else { - "Create directory".into() - } - } - - fn run( - self: Arc, - input: Self::Input, - _event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task> { - let project_path = match self.project.read(cx).find_project_path(&input.path, cx) { - Some(project_path) => project_path, - None => { - return Task::ready(Err(anyhow!("Path to create was outside the project"))); - } - }; - let destination_path: Arc = input.path.as_str().into(); - - let create_entry = self.project.update(cx, |project, cx| { - project.create_entry(project_path.clone(), true, cx) - }); - - cx.spawn(async move |_cx| { - create_entry - .await - .with_context(|| format!("Creating directory {destination_path}"))?; - - Ok(format!("Created directory {destination_path}")) - }) - } -} diff --git a/crates/agent2/src/tools/delete_path_tool.rs b/crates/agent2/src/tools/delete_path_tool.rs deleted file mode 100644 index 0f9641127f..0000000000 --- a/crates/agent2/src/tools/delete_path_tool.rs +++ /dev/null @@ -1,136 +0,0 @@ -use crate::{AgentTool, ToolCallEventStream}; -use action_log::ActionLog; -use agent_client_protocol::ToolKind; -use anyhow::{Context as _, Result, anyhow}; -use futures::{SinkExt, StreamExt, channel::mpsc}; -use gpui::{App, AppContext, Entity, SharedString, Task}; -use project::{Project, ProjectPath}; -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. -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct DeletePathToolInput { - /// The path of the file or directory to delete. - /// - /// - /// If the project has the following files: - /// - /// - directory1/a/something.txt - /// - directory2/a/things.txt - /// - directory3/a/other.txt - /// - /// You can delete the first file by providing a path of "directory1/a/something.txt" - /// - pub path: String, -} - -pub struct DeletePathTool { - project: Entity, - action_log: Entity, -} - -impl DeletePathTool { - pub fn new(project: Entity, action_log: Entity) -> Self { - Self { - project, - action_log, - } - } -} - -impl AgentTool for DeletePathTool { - type Input = DeletePathToolInput; - type Output = String; - - fn name() -> &'static str { - "delete_path" - } - - fn kind() -> ToolKind { - ToolKind::Delete - } - - fn initial_title(&self, input: Result) -> SharedString { - if let Ok(input) = input { - format!("Delete “`{}`”", input.path).into() - } else { - "Delete path".into() - } - } - - fn run( - self: Arc, - input: Self::Input, - _event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task> { - let path = input.path; - let Some(project_path) = self.project.read(cx).find_project_path(&path, cx) else { - return Task::ready(Err(anyhow!( - "Couldn't delete {path} because that path isn't in this project." - ))); - }; - - let Some(worktree) = self - .project - .read(cx) - .worktree_for_id(project_path.worktree_id, cx) - else { - return Task::ready(Err(anyhow!( - "Couldn't delete {path} because that path isn't in this project." - ))); - }; - - let worktree_snapshot = worktree.read(cx).snapshot(); - let (mut paths_tx, mut paths_rx) = mpsc::channel(256); - cx.background_spawn({ - let project_path = project_path.clone(); - async move { - for entry in - worktree_snapshot.traverse_from_path(true, false, false, &project_path.path) - { - if !entry.path.starts_with(&project_path.path) { - break; - } - paths_tx - .send(ProjectPath { - worktree_id: project_path.worktree_id, - path: entry.path.clone(), - }) - .await?; - } - anyhow::Ok(()) - } - }) - .detach(); - - let project = self.project.clone(); - let action_log = self.action_log.clone(); - cx.spawn(async move |cx| { - while let Some(path) = paths_rx.next().await { - if let Ok(buffer) = project - .update(cx, |project, cx| project.open_buffer(path, cx))? - .await - { - action_log.update(cx, |action_log, cx| { - action_log.will_delete_buffer(buffer.clone(), cx) - })?; - } - } - - let deletion_task = project - .update(cx, |project, cx| { - project.delete_file(project_path, false, cx) - })? - .with_context(|| { - format!("Couldn't delete {path} because that path isn't in this project.") - })?; - deletion_task - .await - .with_context(|| format!("Deleting {path}"))?; - Ok(format!("Deleted {path}")) - }) - } -} diff --git a/crates/agent2/src/tools/diagnostics_tool.rs b/crates/agent2/src/tools/diagnostics_tool.rs deleted file mode 100644 index 558bb918ce..0000000000 --- a/crates/agent2/src/tools/diagnostics_tool.rs +++ /dev/null @@ -1,163 +0,0 @@ -use crate::{AgentTool, ToolCallEventStream}; -use agent_client_protocol as acp; -use anyhow::{Result, anyhow}; -use gpui::{App, Entity, Task}; -use language::{DiagnosticSeverity, OffsetRangeExt}; -use project::Project; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::{fmt::Write, path::Path, sync::Arc}; -use ui::SharedString; -use util::markdown::MarkdownInlineCode; - -/// Get errors and warnings for the project or a specific file. -/// -/// This tool can be invoked after a series of edits to determine if further edits are necessary, or if the user asks to fix errors or warnings in their codebase. -/// -/// When a path is provided, shows all diagnostics for that specific file. -/// When no path is provided, shows a summary of error and warning counts for all files in the project. -/// -/// -/// To get diagnostics for a specific file: -/// { -/// "path": "src/main.rs" -/// } -/// -/// To get a project-wide diagnostic summary: -/// {} -/// -/// -/// -/// - If you think you can fix a diagnostic, make 1-2 attempts and then give up. -/// - Don't remove code you've generated just because you can't fix an error. The user can help you fix it. -/// -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct DiagnosticsToolInput { - /// The path to get diagnostics for. If not provided, returns a project-wide summary. - /// - /// This path should never be absolute, and the first component - /// of the path should always be a root directory in a project. - /// - /// - /// If the project has the following root directories: - /// - /// - lorem - /// - ipsum - /// - /// If you wanna access diagnostics for `dolor.txt` in `ipsum`, you should use the path `ipsum/dolor.txt`. - /// - pub path: Option, -} - -pub struct DiagnosticsTool { - project: Entity, -} - -impl DiagnosticsTool { - pub fn new(project: Entity) -> Self { - Self { project } - } -} - -impl AgentTool for DiagnosticsTool { - type Input = DiagnosticsToolInput; - type Output = String; - - fn name() -> &'static str { - "diagnostics" - } - - fn kind() -> acp::ToolKind { - acp::ToolKind::Read - } - - fn initial_title(&self, input: Result) -> SharedString { - if let Some(path) = input.ok().and_then(|input| match input.path { - Some(path) if !path.is_empty() => Some(path), - _ => None, - }) { - format!("Check diagnostics for {}", MarkdownInlineCode(&path)).into() - } else { - "Check project diagnostics".into() - } - } - - fn run( - self: Arc, - input: Self::Input, - _event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task> { - match input.path { - Some(path) if !path.is_empty() => { - let Some(project_path) = self.project.read(cx).find_project_path(&path, cx) else { - return Task::ready(Err(anyhow!("Could not find path {path} in project",))); - }; - - let buffer = self - .project - .update(cx, |project, cx| project.open_buffer(project_path, cx)); - - cx.spawn(async move |cx| { - let mut output = String::new(); - let buffer = buffer.await?; - let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; - - for (_, group) in snapshot.diagnostic_groups(None) { - let entry = &group.entries[group.primary_ix]; - let range = entry.range.to_point(&snapshot); - let severity = match entry.diagnostic.severity { - DiagnosticSeverity::ERROR => "error", - DiagnosticSeverity::WARNING => "warning", - _ => continue, - }; - - writeln!( - output, - "{} at line {}: {}", - severity, - range.start.row + 1, - entry.diagnostic.message - )?; - } - - if output.is_empty() { - Ok("File doesn't have errors or warnings!".to_string()) - } else { - Ok(output) - } - }) - } - _ => { - let project = self.project.read(cx); - let mut output = String::new(); - let mut has_diagnostics = false; - - for (project_path, _, summary) in project.diagnostic_summaries(true, cx) { - if summary.error_count > 0 || summary.warning_count > 0 { - let Some(worktree) = project.worktree_for_id(project_path.worktree_id, cx) - else { - continue; - }; - - has_diagnostics = true; - output.push_str(&format!( - "{}: {} error(s), {} warning(s)\n", - Path::new(worktree.read(cx).root_name()) - .join(project_path.path) - .display(), - summary.error_count, - summary.warning_count - )); - } - } - - if has_diagnostics { - Task::ready(Ok(output)) - } else { - Task::ready(Ok("No errors or warnings found in the project.".into())) - } - } - } - } -} diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs deleted file mode 100644 index f86bfd25f7..0000000000 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ /dev/null @@ -1,1657 +0,0 @@ -use crate::{AgentTool, Thread, ToolCallEventStream}; -use acp_thread::Diff; -use agent_client_protocol::{self as acp, ToolCallLocation, ToolCallUpdateFields}; -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, WeakEntity}; -use indoc::formatdoc; -use language::language_settings::{self, FormatOnSave}; -use language::{LanguageRegistry, ToPoint}; -use language_model::LanguageModelToolResultContent; -use paths; -use project::lsp_store::{FormatTrigger, LspFormatTarget}; -use project::{Project, ProjectPath}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::Settings; -use smol::stream::StreamExt as _; -use std::path::{Path, PathBuf}; -use std::sync::Arc; -use ui::SharedString; -use util::ResultExt; - -const DEFAULT_UI_TEXT: &str = "Editing file"; - -/// This is a tool for creating a new file or editing an existing file. For moving or renaming files, you should generally use the `terminal` tool with the 'mv' command instead. -/// -/// Before using this tool: -/// -/// 1. Use the `read_file` tool to understand the file's contents and context -/// -/// 2. Verify the directory path is correct (only applicable when creating new files): -/// - 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. - /// - /// 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. - /// - /// Fix API endpoint URLs - /// Update copyright year in `page_footer` - /// - /// 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. - /// - /// The following examples assume we have two root directories in the project: - /// - /a/b/backend - /// - /c/d/frontend - /// - /// - /// `backend/src/main.rs` - /// - /// Notice how the file path starts with `backend`. Without that, the path would be ambiguous and the call would fail! - /// - /// - /// - /// `frontend/db.js` - /// - 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. - pub mode: EditFileMode, -} - -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -struct EditFileToolPartialInput { - #[serde(default)] - path: String, - #[serde(default)] - display_description: String, -} - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "lowercase")] -pub enum EditFileMode { - Edit, - Create, - Overwrite, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct EditFileToolOutput { - #[serde(alias = "original_path")] - input_path: PathBuf, - new_text: String, - old_text: Arc, - #[serde(default)] - diff: String, - #[serde(alias = "raw_output")] - edit_agent_output: EditAgentOutput, -} - -impl From for LanguageModelToolResultContent { - fn from(output: EditFileToolOutput) -> Self { - if output.diff.is_empty() { - "No edits were made.".into() - } else { - format!( - "Edited {}:\n\n```diff\n{}\n```", - output.input_path.display(), - output.diff - ) - .into() - } - } -} - -pub struct EditFileTool { - thread: WeakEntity, - language_registry: Arc, -} - -impl EditFileTool { - pub fn new(thread: WeakEntity, language_registry: Arc) -> Self { - Self { - thread, - language_registry, - } - } - - fn authorize( - &self, - input: &EditFileToolInput, - event_stream: &ToolCallEventStream, - cx: &mut App, - ) -> Task> { - if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions { - return Task::ready(Ok(())); - } - - // If any path component matches the local settings folder, then this could affect - // the editor in ways beyond the project source, so prompt. - let local_settings_folder = paths::local_settings_folder_relative_path(); - let path = Path::new(&input.path); - if path - .components() - .any(|component| component.as_os_str() == local_settings_folder.as_os_str()) - { - return event_stream.authorize( - format!("{} (local settings)", input.display_description), - cx, - ); - } - - // 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) - && 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 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. - if project_path.is_some() { - Task::ready(Ok(())) - } else { - event_stream.authorize(&input.display_description, cx) - } - } -} - -impl AgentTool for EditFileTool { - type Input = EditFileToolInput; - type Output = EditFileToolOutput; - - fn name() -> &'static str { - "edit_file" - } - - fn kind() -> acp::ToolKind { - acp::ToolKind::Edit - } - - fn initial_title(&self, input: Result) -> SharedString { - match input { - Ok(input) => input.display_description.into(), - Err(raw_input) => { - if let Some(input) = - serde_json::from_value::(raw_input).ok() - { - 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() - } - } - } - - fn run( - self: Arc, - input: Self::Input, - event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task> { - 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))), - }; - let abs_path = project.read(cx).absolute_path(&project_path, cx); - if let Some(abs_path) = abs_path.clone() { - event_stream.update_fields(ToolCallUpdateFields { - locations: Some(vec![acp::ToolCallLocation { - path: abs_path, - line: None, - }]), - ..Default::default() - }); - } - - 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, - project.clone(), - action_log.clone(), - // TODO: move edit agent to this crate so we can use our templates - assistant_tools::templates::Templates::new(), - edit_format, - ); - - let buffer = project - .update(cx, |project, cx| { - project.open_buffer(project_path.clone(), cx) - })? - .await?; - - 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 - .background_spawn({ - let old_snapshot = old_snapshot.clone(); - async move { Arc::new(old_snapshot.text()) } - }) - .await; - - - let (output, mut events) = if matches!(input.mode, EditFileMode::Edit) { - edit_agent.edit( - buffer.clone(), - input.display_description.clone(), - &request, - cx, - ) - } else { - edit_agent.overwrite( - buffer.clone(), - input.display_description.clone(), - &request, - cx, - ) - }; - - let mut hallucinated_old_text = false; - let mut ambiguous_ranges = Vec::new(); - let mut emitted_location = false; - while let Some(event) = events.next().await { - match event { - EditAgentOutputEvent::Edited(range) => { - if !emitted_location { - let line = buffer.update(cx, |buffer, _cx| { - range.start.to_point(&buffer.snapshot()).row - }).ok(); - if let Some(abs_path) = abs_path.clone() { - event_stream.update_fields(ToolCallUpdateFields { - locations: Some(vec![ToolCallLocation { path: abs_path, line }]), - ..Default::default() - }); - } - emitted_location = true; - } - }, - EditAgentOutputEvent::UnresolvedEditRange => hallucinated_old_text = true, - EditAgentOutputEvent::AmbiguousEditRange(ranges) => ambiguous_ranges = ranges, - EditAgentOutputEvent::ResolvingEditRange(range) => { - diff.update(cx, |card, cx| card.reveal_range(range.clone(), cx))?; - // if !emitted_location { - // let line = buffer.update(cx, |buffer, _cx| { - // range.start.to_point(&buffer.snapshot()).row - // }).ok(); - // if let Some(abs_path) = abs_path.clone() { - // event_stream.update_fields(ToolCallUpdateFields { - // locations: Some(vec![ToolCallLocation { path: abs_path, line }]), - // ..Default::default() - // }); - // } - // } - } - } - } - - // If format_on_save is enabled, format the buffer - let format_on_save_enabled = buffer - .read_with(cx, |buffer, cx| { - let settings = language_settings::language_settings( - buffer.language().map(|l| l.name()), - buffer.file(), - cx, - ); - settings.format_on_save != FormatOnSave::Off - }) - .unwrap_or(false); - - let edit_agent_output = output.await?; - - if format_on_save_enabled { - action_log.update(cx, |log, cx| { - log.buffer_edited(buffer.clone(), cx); - })?; - - let format_task = project.update(cx, |project, cx| { - project.format( - HashSet::from_iter([buffer.clone()]), - LspFormatTarget::Buffers, - false, // Don't push to history since the tool did it. - FormatTrigger::Save, - cx, - ) - })?; - format_task.await.log_err(); - } - - project - .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))? - .await?; - - action_log.update(cx, |log, cx| { - log.buffer_edited(buffer.clone(), cx); - })?; - - let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; - let (new_text, unified_diff) = cx - .background_spawn({ - let new_snapshot = new_snapshot.clone(); - let old_text = old_text.clone(); - async move { - let new_text = new_snapshot.text(); - let diff = language::unified_diff(&old_text, &new_text); - (new_text, diff) - } - }) - .await; - - let input_path = input.path.display(); - if unified_diff.is_empty() { - anyhow::ensure!( - !hallucinated_old_text, - formatdoc! {" - Some edits were produced but none of them could be applied. - Read the relevant sections of {input_path} again so that - I can perform the requested edits. - "} - ); - anyhow::ensure!( - ambiguous_ranges.is_empty(), - { - let line_numbers = ambiguous_ranges - .iter() - .map(|range| range.start.to_string()) - .collect::>() - .join(", "); - formatdoc! {" - matches more than one position in the file (lines: {line_numbers}). Read the - relevant sections of {input_path} again and extend so - that I can perform the requested edits. - "} - } - ); - } - - Ok(EditFileToolOutput { - input_path: input.path, - 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: -/// -/// - For `edit` and `overwrite`, the path must point to an existing file. -/// - For `create`, the file must not already exist, but it's parent dir must exist. -fn resolve_path( - input: &EditFileToolInput, - project: Entity, - cx: &mut App, -) -> Result { - let project = project.read(cx); - - match input.mode { - EditFileMode::Edit | EditFileMode::Overwrite => { - let path = project - .find_project_path(&input.path, cx) - .context("Can't edit file: path not found")?; - - let entry = project - .entry_for_path(&path, cx) - .context("Can't edit file: path not found")?; - - anyhow::ensure!(entry.is_file(), "Can't edit file: path is a directory"); - Ok(path) - } - - EditFileMode::Create => { - if let Some(path) = project.find_project_path(&input.path, cx) { - anyhow::ensure!( - project.entry_for_path(&path, cx).is_none(), - "Can't create file: file already exists" - ); - } - - let parent_path = input - .path - .parent() - .context("Can't create file: incorrect path")?; - - let parent_project_path = project.find_project_path(&parent_path, cx); - - let parent_entry = parent_project_path - .as_ref() - .and_then(|path| project.entry_for_path(path, cx)) - .context("Can't create file: parent directory doesn't exist")?; - - anyhow::ensure!( - parent_entry.is_dir(), - "Can't create file: parent is not a directory" - ); - - let file_name = input - .path - .file_name() - .context("Can't create file: invalid filename")?; - - let new_file_path = parent_project_path.map(|parent| ProjectPath { - path: Arc::from(parent.path.join(file_name)), - ..parent - }); - - new_file_path.context("Can't create file") - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ContextServerRegistry, Templates}; - 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 util::path; - - #[gpui::test] - async fn test_edit_nonexistent_file(cx: &mut TestAppContext) { - init_test(cx); - - 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 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, - cx.new(|_cx| ProjectContext::default()), - context_server_registry, - Templates::new(), - Some(model), - cx, - ) - }); - let result = cx - .update(|cx| { - let input = EditFileToolInput { - display_description: "Some edit".into(), - path: "root/nonexistent_file.txt".into(), - mode: EditFileMode::Edit, - }; - Arc::new(EditFileTool::new(thread.downgrade(), language_registry)).run( - input, - ToolCallEventStream::test().0, - cx, - ) - }) - .await; - assert_eq!( - result.unwrap_err().to_string(), - "Can't edit file: path not found" - ); - } - - #[gpui::test] - async fn test_resolve_path_for_creating_file(cx: &mut TestAppContext) { - let mode = &EditFileMode::Create; - - let result = test_resolve_path(mode, "root/new.txt", cx); - assert_resolved_path_eq(result.await, "new.txt"); - - let result = test_resolve_path(mode, "new.txt", cx); - assert_resolved_path_eq(result.await, "new.txt"); - - let result = test_resolve_path(mode, "dir/new.txt", cx); - assert_resolved_path_eq(result.await, "dir/new.txt"); - - let result = test_resolve_path(mode, "root/dir/subdir/existing.txt", cx); - assert_eq!( - result.await.unwrap_err().to_string(), - "Can't create file: file already exists" - ); - - let result = test_resolve_path(mode, "root/dir/nonexistent_dir/new.txt", cx); - assert_eq!( - result.await.unwrap_err().to_string(), - "Can't create file: parent directory doesn't exist" - ); - } - - #[gpui::test] - async fn test_resolve_path_for_editing_file(cx: &mut TestAppContext) { - let mode = &EditFileMode::Edit; - - let path_with_root = "root/dir/subdir/existing.txt"; - let path_without_root = "dir/subdir/existing.txt"; - let result = test_resolve_path(mode, path_with_root, cx); - assert_resolved_path_eq(result.await, path_without_root); - - let result = test_resolve_path(mode, path_without_root, cx); - assert_resolved_path_eq(result.await, path_without_root); - - let result = test_resolve_path(mode, "root/nonexistent.txt", cx); - assert_eq!( - result.await.unwrap_err().to_string(), - "Can't edit file: path not found" - ); - - let result = test_resolve_path(mode, "root/dir", cx); - assert_eq!( - result.await.unwrap_err().to_string(), - "Can't edit file: path is a directory" - ); - } - - async fn test_resolve_path( - mode: &EditFileMode, - path: &str, - cx: &mut TestAppContext, - ) -> anyhow::Result { - init_test(cx); - - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "dir": { - "subdir": { - "existing.txt": "hello" - } - } - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - - let input = EditFileToolInput { - display_description: "Some edit".into(), - path: path.into(), - mode: mode.clone(), - }; - - cx.update(|cx| resolve_path(&input, project, cx)) - } - - fn assert_resolved_path_eq(path: anyhow::Result, expected: &str) { - let actual = path - .expect("Should return valid path") - .path - .to_str() - .unwrap() - .replace("\\", "/"); // Naive Windows paths normalization - assert_eq!(actual, expected); - } - - #[gpui::test] - async fn test_format_on_save(cx: &mut TestAppContext) { - init_test(cx); - - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree("/root", json!({"src": {}})).await; - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - - // Set up a Rust language with LSP formatting support - let rust_language = Arc::new(language::Language::new( - language::LanguageConfig { - name: "Rust".into(), - matcher: language::LanguageMatcher { - path_suffixes: vec!["rs".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(rust_language); - - let mut fake_language_servers = language_registry.register_fake_lsp( - "Rust", - language::FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - document_formatting_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }, - ..Default::default() - }, - ); - - // Create the file - fs.save( - path!("/root/src/main.rs").as_ref(), - &"initial content".into(), - language::LineEnding::Unix, - ) - .await - .unwrap(); - - // Open the buffer to trigger LSP initialization - let buffer = project - .update(cx, |project, cx| { - project.open_local_buffer(path!("/root/src/main.rs"), cx) - }) - .await - .unwrap(); - - // Register the buffer with language servers - let _handle = project.update(cx, |project, cx| { - project.register_buffer_with_language_servers(&buffer, cx) - }); - - const UNFORMATTED_CONTENT: &str = "fn main() {println!(\"Hello!\");}\n"; - const FORMATTED_CONTENT: &str = - "This file was formatted by the fake formatter in the test.\n"; - - // Get the fake language server and set up formatting handler - let fake_language_server = fake_language_servers.next().await.unwrap(); - fake_language_server.set_request_handler::({ - |_, _| async move { - Ok(Some(vec![lsp::TextEdit { - range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(1, 0)), - new_text: FORMATTED_CONTENT.to_string(), - }])) - } - }); - - 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, - cx.new(|_cx| ProjectContext::default()), - context_server_registry, - Templates::new(), - Some(model.clone()), - cx, - ) - }); - - // First, test with format_on_save enabled - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::( - cx, - |settings| { - settings.defaults.format_on_save = Some(FormatOnSave::On); - settings.defaults.formatter = - Some(language::language_settings::SelectedFormatter::Auto); - }, - ); - }); - }); - - // Have the model stream unformatted content - let edit_result = { - let edit_task = cx.update(|cx| { - let input = EditFileToolInput { - display_description: "Create main function".into(), - path: "root/src/main.rs".into(), - mode: EditFileMode::Overwrite, - }; - Arc::new(EditFileTool::new( - thread.downgrade(), - language_registry.clone(), - )) - .run(input, ToolCallEventStream::test().0, cx) - }); - - // Stream the unformatted content - cx.executor().run_until_parked(); - model.send_last_completion_stream_text_chunk(UNFORMATTED_CONTENT.to_string()); - model.end_last_completion_stream(); - - edit_task.await - }; - assert!(edit_result.is_ok()); - - // Wait for any async operations (e.g. formatting) to complete - cx.executor().run_until_parked(); - - // Read the file to verify it was formatted automatically - let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap(); - assert_eq!( - // Ignore carriage returns on Windows - new_content.replace("\r\n", "\n"), - FORMATTED_CONTENT, - "Code should be formatted when format_on_save is enabled" - ); - - 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, - "BUG: Buffer is incorrectly marked as stale after format-on-save. Found {} stale buffers. \ - This causes the agent to think the file was modified externally when it was just formatted.", - stale_buffer_count - ); - - // Next, test with format_on_save disabled - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::( - cx, - |settings| { - settings.defaults.format_on_save = Some(FormatOnSave::Off); - }, - ); - }); - }); - - // Stream unformatted edits again - let edit_result = { - let edit_task = cx.update(|cx| { - let input = EditFileToolInput { - display_description: "Update main function".into(), - path: "root/src/main.rs".into(), - mode: EditFileMode::Overwrite, - }; - Arc::new(EditFileTool::new(thread.downgrade(), language_registry)).run( - input, - ToolCallEventStream::test().0, - cx, - ) - }); - - // Stream the unformatted content - cx.executor().run_until_parked(); - model.send_last_completion_stream_text_chunk(UNFORMATTED_CONTENT.to_string()); - model.end_last_completion_stream(); - - edit_task.await - }; - assert!(edit_result.is_ok()); - - // Wait for any async operations (e.g. formatting) to complete - cx.executor().run_until_parked(); - - // Verify the file was not formatted - let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap(); - assert_eq!( - // Ignore carriage returns on Windows - new_content.replace("\r\n", "\n"), - UNFORMATTED_CONTENT, - "Code should not be formatted when format_on_save is disabled" - ); - } - - #[gpui::test] - async fn test_remove_trailing_whitespace(cx: &mut TestAppContext) { - init_test(cx); - - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree("/root", json!({"src": {}})).await; - - // Create a simple file with trailing whitespace - fs.save( - path!("/root/src/main.rs").as_ref(), - &"initial content".into(), - language::LineEnding::Unix, - ) - .await - .unwrap(); - - 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 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, - cx.new(|_cx| ProjectContext::default()), - context_server_registry, - Templates::new(), - Some(model.clone()), - cx, - ) - }); - - // First, test with remove_trailing_whitespace_on_save enabled - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::( - cx, - |settings| { - settings.defaults.remove_trailing_whitespace_on_save = Some(true); - }, - ); - }); - }); - - const CONTENT_WITH_TRAILING_WHITESPACE: &str = - "fn main() { \n println!(\"Hello!\"); \n}\n"; - - // Have the model stream content that contains trailing whitespace - let edit_result = { - let edit_task = cx.update(|cx| { - let input = EditFileToolInput { - display_description: "Create main function".into(), - path: "root/src/main.rs".into(), - mode: EditFileMode::Overwrite, - }; - Arc::new(EditFileTool::new( - thread.downgrade(), - language_registry.clone(), - )) - .run(input, ToolCallEventStream::test().0, cx) - }); - - // Stream the content with trailing whitespace - cx.executor().run_until_parked(); - model.send_last_completion_stream_text_chunk( - CONTENT_WITH_TRAILING_WHITESPACE.to_string(), - ); - model.end_last_completion_stream(); - - edit_task.await - }; - assert!(edit_result.is_ok()); - - // Wait for any async operations (e.g. formatting) to complete - cx.executor().run_until_parked(); - - // Read the file to verify trailing whitespace was removed automatically - assert_eq!( - // Ignore carriage returns on Windows - fs.load(path!("/root/src/main.rs").as_ref()) - .await - .unwrap() - .replace("\r\n", "\n"), - "fn main() {\n println!(\"Hello!\");\n}\n", - "Trailing whitespace should be removed when remove_trailing_whitespace_on_save is enabled" - ); - - // Next, test with remove_trailing_whitespace_on_save disabled - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::( - cx, - |settings| { - settings.defaults.remove_trailing_whitespace_on_save = Some(false); - }, - ); - }); - }); - - // Stream edits again with trailing whitespace - let edit_result = { - let edit_task = cx.update(|cx| { - let input = EditFileToolInput { - display_description: "Update main function".into(), - path: "root/src/main.rs".into(), - mode: EditFileMode::Overwrite, - }; - Arc::new(EditFileTool::new(thread.downgrade(), language_registry)).run( - input, - ToolCallEventStream::test().0, - cx, - ) - }); - - // Stream the content with trailing whitespace - cx.executor().run_until_parked(); - model.send_last_completion_stream_text_chunk( - CONTENT_WITH_TRAILING_WHITESPACE.to_string(), - ); - model.end_last_completion_stream(); - - edit_task.await - }; - assert!(edit_result.is_ok()); - - // Wait for any async operations (e.g. formatting) to complete - cx.executor().run_until_parked(); - - // Verify the file still has trailing whitespace - // Read the file again - it should still have trailing whitespace - let final_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap(); - assert_eq!( - // Ignore carriage returns on Windows - final_content.replace("\r\n", "\n"), - CONTENT_WITH_TRAILING_WHITESPACE, - "Trailing whitespace should remain when remove_trailing_whitespace_on_save is disabled" - ); - } - - #[gpui::test] - async fn test_authorize(cx: &mut TestAppContext) { - init_test(cx); - let fs = project::FakeFs::new(cx.executor()); - 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 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, - cx.new(|_cx| ProjectContext::default()), - context_server_registry, - Templates::new(), - Some(model.clone()), - cx, - ) - }); - let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry)); - fs.insert_tree("/root", json!({})).await; - - // Test 1: Path with .zed component should require confirmation - let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); - let _auth = cx.update(|cx| { - tool.authorize( - &EditFileToolInput { - display_description: "test 1".into(), - path: ".zed/settings.json".into(), - mode: EditFileMode::Edit, - }, - &stream_tx, - cx, - ) - }); - - let event = stream_rx.expect_authorization().await; - 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(); - let _auth = cx.update(|cx| { - tool.authorize( - &EditFileToolInput { - display_description: "test 2".into(), - path: "/etc/hosts".into(), - mode: EditFileMode::Edit, - }, - &stream_tx, - cx, - ) - }); - - let event = stream_rx.expect_authorization().await; - 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(); - cx.update(|cx| { - tool.authorize( - &EditFileToolInput { - display_description: "test 3".into(), - path: "root/src/main.rs".into(), - mode: EditFileMode::Edit, - }, - &stream_tx, - cx, - ) - }) - .await - .unwrap(); - assert!(stream_rx.try_next().is_err()); - - // Test 4: Path with .zed in the middle should require confirmation - let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); - let _auth = cx.update(|cx| { - tool.authorize( - &EditFileToolInput { - display_description: "test 4".into(), - path: "root/.zed/tasks.json".into(), - mode: EditFileMode::Edit, - }, - &stream_tx, - cx, - ) - }); - let event = stream_rx.expect_authorization().await; - 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| { - let mut settings = agent_settings::AgentSettings::get_global(cx).clone(); - settings.always_allow_tool_actions = true; - agent_settings::AgentSettings::override_global(settings, cx); - }); - - let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); - cx.update(|cx| { - tool.authorize( - &EditFileToolInput { - display_description: "test 5.1".into(), - path: ".zed/settings.json".into(), - mode: EditFileMode::Edit, - }, - &stream_tx, - cx, - ) - }) - .await - .unwrap(); - assert!(stream_rx.try_next().is_err()); - - let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); - cx.update(|cx| { - tool.authorize( - &EditFileToolInput { - display_description: "test 5.2".into(), - path: "/etc/hosts".into(), - mode: EditFileMode::Edit, - }, - &stream_tx, - cx, - ) - }) - .await - .unwrap(); - assert!(stream_rx.try_next().is_err()); - } - - #[gpui::test] - async fn test_authorize_global_config(cx: &mut TestAppContext) { - init_test(cx); - 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 model = Arc::new(FakeLanguageModel::default()); - let thread = cx.new(|cx| { - Thread::new( - project, - cx.new(|_cx| ProjectContext::default()), - context_server_registry, - Templates::new(), - Some(model.clone()), - cx, - ) - }); - let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry)); - - // Test global config paths - these should require confirmation if they exist and are outside the project - let test_cases = vec![ - ( - "/etc/hosts", - true, - "System file should require confirmation", - ), - ( - "/usr/local/bin/script", - true, - "System bin file should require confirmation", - ), - ( - "project/normal_file.rs", - false, - "Normal project file should not require confirmation", - ), - ]; - - for (path, should_confirm, description) in test_cases { - let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); - let auth = cx.update(|cx| { - tool.authorize( - &EditFileToolInput { - display_description: "Edit file".into(), - path: path.into(), - mode: EditFileMode::Edit, - }, - &stream_tx, - cx, - ) - }); - - if should_confirm { - stream_rx.expect_authorization().await; - } else { - auth.await.unwrap(); - assert!( - stream_rx.try_next().is_err(), - "Failed for case: {} - path: {} - expected no confirmation but got one", - description, - path - ); - } - } - } - - #[gpui::test] - async fn test_needs_confirmation_with_multiple_worktrees(cx: &mut TestAppContext) { - init_test(cx); - let fs = project::FakeFs::new(cx.executor()); - - // Create multiple worktree directories - fs.insert_tree( - "/workspace/frontend", - json!({ - "src": { - "main.js": "console.log('frontend');" - } - }), - ) - .await; - fs.insert_tree( - "/workspace/backend", - json!({ - "src": { - "main.rs": "fn main() {}" - } - }), - ) - .await; - fs.insert_tree( - "/workspace/shared", - json!({ - ".zed": { - "settings.json": "{}" - } - }), - ) - .await; - - // Create project with multiple worktrees - let project = Project::test( - fs.clone(), - [ - path!("/workspace/frontend").as_ref(), - path!("/workspace/backend").as_ref(), - path!("/workspace/shared").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 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, - ) - }); - let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry)); - - // Test files in different worktrees - let test_cases = vec![ - ("frontend/src/main.js", false, "File in first worktree"), - ("backend/src/main.rs", false, "File in second worktree"), - ( - "shared/.zed/settings.json", - true, - ".zed file in third worktree", - ), - ("/etc/hosts", true, "Absolute path outside all worktrees"), - ( - "../outside/file.txt", - true, - "Relative path outside worktrees", - ), - ]; - - for (path, should_confirm, description) in test_cases { - let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); - let auth = cx.update(|cx| { - tool.authorize( - &EditFileToolInput { - display_description: "Edit file".into(), - path: path.into(), - mode: EditFileMode::Edit, - }, - &stream_tx, - cx, - ) - }); - - if should_confirm { - stream_rx.expect_authorization().await; - } else { - auth.await.unwrap(); - assert!( - stream_rx.try_next().is_err(), - "Failed for case: {} - path: {} - expected no confirmation but got one", - description, - path - ); - } - } - } - - #[gpui::test] - async fn test_needs_confirmation_edge_cases(cx: &mut TestAppContext) { - init_test(cx); - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree( - "/project", - json!({ - ".zed": { - "settings.json": "{}" - }, - "src": { - ".zed": { - "local.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 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, - ) - }); - let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry)); - - // Test edge cases - let test_cases = vec![ - // Empty path - find_project_path returns Some for empty paths - ("", false, "Empty path is treated as project root"), - // Root directory - ("/", true, "Root directory should be outside project"), - // Parent directory references - find_project_path resolves these - ( - "project/../other", - false, - "Path with .. is resolved by find_project_path", - ), - ( - "project/./src/file.rs", - false, - "Path with . should work normally", - ), - // Windows-style paths (if on Windows) - #[cfg(target_os = "windows")] - ("C:\\Windows\\System32\\hosts", true, "Windows system path"), - #[cfg(target_os = "windows")] - ("project\\src\\main.rs", false, "Windows-style project path"), - ]; - - for (path, should_confirm, description) in test_cases { - let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); - let auth = cx.update(|cx| { - tool.authorize( - &EditFileToolInput { - display_description: "Edit file".into(), - path: path.into(), - mode: EditFileMode::Edit, - }, - &stream_tx, - cx, - ) - }); - - if should_confirm { - stream_rx.expect_authorization().await; - } else { - auth.await.unwrap(); - assert!( - stream_rx.try_next().is_err(), - "Failed for case: {} - path: {} - expected no confirmation but got one", - description, - path - ); - } - } - } - - #[gpui::test] - async fn test_needs_confirmation_with_different_modes(cx: &mut TestAppContext) { - init_test(cx); - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree( - "/project", - json!({ - "existing.txt": "content", - ".zed": { - "settings.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 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, - ) - }); - let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry)); - - // Test different EditFileMode values - let modes = vec![ - EditFileMode::Edit, - EditFileMode::Create, - EditFileMode::Overwrite, - ]; - - for mode in modes { - // Test .zed path with different modes - let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); - let _auth = cx.update(|cx| { - tool.authorize( - &EditFileToolInput { - display_description: "Edit settings".into(), - path: "project/.zed/settings.json".into(), - mode: mode.clone(), - }, - &stream_tx, - cx, - ) - }); - - stream_rx.expect_authorization().await; - - // Test outside path with different modes - let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); - let _auth = cx.update(|cx| { - tool.authorize( - &EditFileToolInput { - display_description: "Edit file".into(), - path: "/outside/file.txt".into(), - mode: mode.clone(), - }, - &stream_tx, - cx, - ) - }); - - stream_rx.expect_authorization().await; - - // Test normal path with different modes - let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); - cx.update(|cx| { - tool.authorize( - &EditFileToolInput { - display_description: "Edit file".into(), - path: "project/normal.txt".into(), - mode: mode.clone(), - }, - &stream_tx, - cx, - ) - }) - .await - .unwrap(); - assert!(stream_rx.try_next().is_err()); - } - } - - #[gpui::test] - async fn test_initial_title_with_partial_input(cx: &mut TestAppContext) { - init_test(cx); - let fs = project::FakeFs::new(cx.executor()); - 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 model = Arc::new(FakeLanguageModel::default()); - let thread = cx.new(|cx| { - Thread::new( - project.clone(), - cx.new(|_cx| ProjectContext::default()), - context_server_registry, - Templates::new(), - Some(model.clone()), - cx, - ) - }); - let tool = Arc::new(EditFileTool::new(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 - ); - } - - #[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(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(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(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) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - language::init(cx); - TelemetrySettings::register(cx); - agent_settings::AgentSettings::register(cx); - Project::init_settings(cx); - }); - } -} diff --git a/crates/agent2/src/tools/fetch_tool.rs b/crates/agent2/src/tools/fetch_tool.rs deleted file mode 100644 index dd97271a79..0000000000 --- a/crates/agent2/src/tools/fetch_tool.rs +++ /dev/null @@ -1,160 +0,0 @@ -use std::rc::Rc; -use std::sync::Arc; -use std::{borrow::Cow, cell::RefCell}; - -use agent_client_protocol as acp; -use anyhow::{Context as _, Result, bail}; -use futures::AsyncReadExt as _; -use gpui::{App, AppContext as _, Task}; -use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown}; -use http_client::{AsyncBody, HttpClientWithUrl}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use ui::SharedString; -use util::markdown::MarkdownEscaped; - -use crate::{AgentTool, ToolCallEventStream}; - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -enum ContentType { - Html, - Plaintext, - Json, -} - -/// Fetches a URL and returns the content as Markdown. -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct FetchToolInput { - /// The URL to fetch. - url: String, -} - -pub struct FetchTool { - http_client: Arc, -} - -impl FetchTool { - pub fn new(http_client: Arc) -> Self { - Self { http_client } - } - - async fn build_message(http_client: Arc, url: &str) -> Result { - let url = if !url.starts_with("https://") && !url.starts_with("http://") { - Cow::Owned(format!("https://{url}")) - } else { - Cow::Borrowed(url) - }; - - let mut response = http_client.get(&url, AsyncBody::default(), true).await?; - - let mut body = Vec::new(); - response - .body_mut() - .read_to_end(&mut body) - .await - .context("error reading 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() - ); - } - - let Some(content_type) = response.headers().get("content-type") else { - bail!("missing Content-Type header"); - }; - let content_type = content_type - .to_str() - .context("invalid Content-Type header")?; - - let content_type = if content_type.starts_with("text/plain") { - ContentType::Plaintext - } else if content_type.starts_with("application/json") { - ContentType::Json - } else { - ContentType::Html - }; - - match content_type { - ContentType::Html => { - let mut handlers: Vec = vec![ - Rc::new(RefCell::new(markdown::WebpageChromeRemover)), - Rc::new(RefCell::new(markdown::ParagraphHandler)), - Rc::new(RefCell::new(markdown::HeadingHandler)), - Rc::new(RefCell::new(markdown::ListHandler)), - Rc::new(RefCell::new(markdown::TableHandler::new())), - Rc::new(RefCell::new(markdown::StyledTextHandler)), - ]; - if url.contains("wikipedia.org") { - use html_to_markdown::structure::wikipedia; - - handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover))); - handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler))); - handlers.push(Rc::new( - RefCell::new(wikipedia::WikipediaCodeHandler::new()), - )); - } else { - handlers.push(Rc::new(RefCell::new(markdown::CodeHandler))); - } - - convert_html_to_markdown(&body[..], &mut handlers) - } - ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()), - ContentType::Json => { - let json: serde_json::Value = serde_json::from_slice(&body)?; - - Ok(format!( - "```json\n{}\n```", - serde_json::to_string_pretty(&json)? - )) - } - } - } -} - -impl AgentTool for FetchTool { - type Input = FetchToolInput; - type Output = String; - - fn name() -> &'static str { - "fetch" - } - - fn kind() -> acp::ToolKind { - acp::ToolKind::Fetch - } - - fn initial_title(&self, input: Result) -> SharedString { - match input { - Ok(input) => format!("Fetch {}", MarkdownEscaped(&input.url)).into(), - Err(_) => "Fetch URL".into(), - } - } - - fn run( - self: Arc, - input: Self::Input, - event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task> { - let authorize = event_stream.authorize(input.url.clone(), cx); - - let text = cx.background_spawn({ - let http_client = self.http_client.clone(); - async move { - authorize.await?; - Self::build_message(http_client, &input.url).await - } - }); - - cx.foreground_executor().spawn(async move { - let text = text.await?; - if text.trim().is_empty() { - bail!("no textual content found"); - } - Ok(text) - }) - } -} diff --git a/crates/agent2/src/tools/find_path_tool.rs b/crates/agent2/src/tools/find_path_tool.rs deleted file mode 100644 index 384bd56e77..0000000000 --- a/crates/agent2/src/tools/find_path_tool.rs +++ /dev/null @@ -1,245 +0,0 @@ -use crate::{AgentTool, ToolCallEventStream}; -use agent_client_protocol as acp; -use anyhow::{Result, anyhow}; -use gpui::{App, AppContext, Entity, SharedString, Task}; -use language_model::LanguageModelToolResultContent; -use project::Project; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::fmt::Write; -use std::{cmp, path::PathBuf, sync::Arc}; -use util::paths::PathMatcher; - -/// Fast file path pattern matching tool that works with any codebase size -/// -/// - Supports glob patterns like "**/*.js" or "src/**/*.ts" -/// - Returns matching file paths sorted alphabetically -/// - Prefer the `grep` tool to this tool when searching for symbols unless you have specific information about paths. -/// - Use this tool when you need to find files by name patterns -/// - Results are paginated with 50 matches per page. Use the optional 'offset' parameter to request subsequent pages. -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct FindPathToolInput { - /// The glob to match against every path in the project. - /// - /// - /// If the project has the following root directories: - /// - /// - directory1/a/something.txt - /// - directory2/a/things.txt - /// - directory3/a/other.txt - /// - /// You can get back the first two paths by providing a glob of "*thing*.txt" - /// - pub glob: String, - /// Optional starting position for paginated results (0-based). - /// When not provided, starts from the beginning. - #[serde(default)] - pub offset: usize, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct FindPathToolOutput { - offset: usize, - current_matches_page: Vec, - all_matches_len: usize, -} - -impl From for LanguageModelToolResultContent { - fn from(output: FindPathToolOutput) -> Self { - if output.current_matches_page.is_empty() { - "No matches found".into() - } else { - let mut llm_output = format!("Found {} total matches.", output.all_matches_len); - if output.all_matches_len > RESULTS_PER_PAGE { - write!( - &mut llm_output, - "\nShowing results {}-{} (provide 'offset' parameter for more results):", - output.offset + 1, - output.offset + output.current_matches_page.len() - ) - .unwrap(); - } - - for mat in output.current_matches_page { - write!(&mut llm_output, "\n{}", mat.display()).unwrap(); - } - - llm_output.into() - } - } -} - -const RESULTS_PER_PAGE: usize = 50; - -pub struct FindPathTool { - project: Entity, -} - -impl FindPathTool { - pub fn new(project: Entity) -> Self { - Self { project } - } -} - -impl AgentTool for FindPathTool { - type Input = FindPathToolInput; - type Output = FindPathToolOutput; - - fn name() -> &'static str { - "find_path" - } - - fn kind() -> acp::ToolKind { - acp::ToolKind::Search - } - - fn initial_title(&self, input: Result) -> SharedString { - let mut title = "Find paths".to_string(); - if let Ok(input) = input { - title.push_str(&format!(" matching “`{}`”", input.glob)); - } - title.into() - } - - fn run( - self: Arc, - input: Self::Input, - event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task> { - let search_paths_task = search_paths(&input.glob, self.project.clone(), cx); - - cx.background_spawn(async move { - let matches = search_paths_task.await?; - let paginated_matches: &[PathBuf] = &matches[cmp::min(input.offset, matches.len()) - ..cmp::min(input.offset + RESULTS_PER_PAGE, matches.len())]; - - event_stream.update_fields(acp::ToolCallUpdateFields { - title: Some(if paginated_matches.is_empty() { - "No matches".into() - } else if paginated_matches.len() == 1 { - "1 match".into() - } else { - format!("{} matches", paginated_matches.len()) - }), - content: Some( - paginated_matches - .iter() - .map(|path| acp::ToolCallContent::Content { - content: acp::ContentBlock::ResourceLink(acp::ResourceLink { - uri: format!("file://{}", path.display()), - name: path.to_string_lossy().into(), - annotations: None, - description: None, - mime_type: None, - size: None, - title: None, - }), - }) - .collect(), - ), - ..Default::default() - }); - - Ok(FindPathToolOutput { - offset: input.offset, - current_matches_page: paginated_matches.to_vec(), - all_matches_len: matches.len(), - }) - }) - } -} - -fn search_paths(glob: &str, project: Entity, cx: &mut App) -> Task>> { - let path_matcher = match PathMatcher::new([ - // Sometimes models try to search for "". In this case, return all paths in the project. - if glob.is_empty() { "*" } else { glob }, - ]) { - Ok(matcher) => matcher, - Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {err}"))), - }; - let snapshots: Vec<_> = project - .read(cx) - .worktrees(cx) - .map(|worktree| worktree.read(cx).snapshot()) - .collect(); - - cx.background_spawn(async move { - let mut results = Vec::new(); - for snapshot in snapshots { - for entry in snapshot.entries(false, 0) { - let root_name = PathBuf::from(snapshot.root_name()); - if path_matcher.is_match(root_name.join(&entry.path)) { - results.push(snapshot.abs_path().join(entry.path.as_ref())); - } - } - } - - Ok(results) - }) -} - -#[cfg(test)] -mod test { - use super::*; - use gpui::TestAppContext; - use project::{FakeFs, Project}; - use settings::SettingsStore; - use util::path; - - #[gpui::test] - async fn test_find_path_tool(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - serde_json::json!({ - "apple": { - "banana": { - "carrot": "1", - }, - "bandana": { - "carbonara": "2", - }, - "endive": "3" - } - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - - let matches = cx - .update(|cx| search_paths("root/**/car*", project.clone(), cx)) - .await - .unwrap(); - assert_eq!( - matches, - &[ - PathBuf::from(path!("/root/apple/banana/carrot")), - PathBuf::from(path!("/root/apple/bandana/carbonara")) - ] - ); - - let matches = cx - .update(|cx| search_paths("**/car*", project.clone(), cx)) - .await - .unwrap(); - assert_eq!( - matches, - &[ - PathBuf::from(path!("/root/apple/banana/carrot")), - PathBuf::from(path!("/root/apple/bandana/carbonara")) - ] - ); - } - - 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); - }); - } -} diff --git a/crates/agent2/src/tools/grep_tool.rs b/crates/agent2/src/tools/grep_tool.rs deleted file mode 100644 index b24e773903..0000000000 --- a/crates/agent2/src/tools/grep_tool.rs +++ /dev/null @@ -1,1182 +0,0 @@ -use crate::{AgentTool, ToolCallEventStream}; -use agent_client_protocol as acp; -use anyhow::{Result, anyhow}; -use futures::StreamExt; -use gpui::{App, Entity, SharedString, Task}; -use language::{OffsetRangeExt, ParseStatus, Point}; -use project::{ - Project, WorktreeSettings, - search::{SearchQuery, SearchResult}, -}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::Settings; -use std::{cmp, fmt::Write, sync::Arc}; -use util::RangeExt; -use util::markdown::MarkdownInlineCode; -use util::paths::PathMatcher; - -/// Searches the contents of files in the project with a regular expression -/// -/// - Prefer this tool to path search when searching for symbols in the project, because you won't need to guess what path it's in. -/// - Supports full regex syntax (eg. "log.*Error", "function\\s+\\w+", etc.) -/// - Pass an `include_pattern` if you know how to narrow your search on the files system -/// - Never use this tool to search for paths. Only search file contents with this tool. -/// - Use this tool when you need to find files containing specific patterns -/// - Results are paginated with 20 matches per page. Use the optional 'offset' parameter to request subsequent pages. -/// - 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. - /// - /// Do NOT specify a path here! This will only be matched against the code **content**. - pub regex: String, - /// A glob pattern for the paths of files to include in the search. - /// Supports standard glob patterns like "**/*.rs" or "src/**/*.ts". - /// If omitted, all files in the project will be searched. - pub include_pattern: Option, - /// Optional starting position for paginated results (0-based). - /// When not provided, starts from the beginning. - #[serde(default)] - pub offset: u32, - /// Whether the regex is case-sensitive. Defaults to false (case-insensitive). - #[serde(default)] - pub case_sensitive: bool, -} - -impl GrepToolInput { - /// Which page of search results this is. - pub fn page(&self) -> u32 { - 1 + (self.offset / RESULTS_PER_PAGE) - } -} - -const RESULTS_PER_PAGE: u32 = 20; - -pub struct GrepTool { - project: Entity, -} - -impl GrepTool { - pub fn new(project: Entity) -> Self { - Self { project } - } -} - -impl AgentTool for GrepTool { - type Input = GrepToolInput; - type Output = String; - - fn name() -> &'static str { - "grep" - } - - fn kind() -> acp::ToolKind { - acp::ToolKind::Search - } - - fn initial_title(&self, input: Result) -> SharedString { - match input { - Ok(input) => { - let page = input.page(); - let regex_str = MarkdownInlineCode(&input.regex); - let case_info = if input.case_sensitive { - " (case-sensitive)" - } else { - "" - }; - - if page > 1 { - format!("Get page {page} of search results for regex {regex_str}{case_info}") - } else { - format!("Search files for regex {regex_str}{case_info}") - } - } - Err(_) => "Search with regex".into(), - } - .into() - } - - fn run( - self: Arc, - input: Self::Input, - _event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task> { - const CONTEXT_LINES: u32 = 2; - const MAX_ANCESTOR_LINES: u32 = 10; - - let include_matcher = match PathMatcher::new( - input - .include_pattern - .as_ref() - .into_iter() - .collect::>(), - ) { - Ok(matcher) => matcher, - Err(error) => { - return Task::ready(Err(anyhow!("invalid include glob pattern: {error}"))); - } - }; - - // Exclude global file_scan_exclusions and private_files settings - let exclude_matcher = { - let global_settings = WorktreeSettings::get_global(cx); - let exclude_patterns = global_settings - .file_scan_exclusions - .sources() - .iter() - .chain(global_settings.private_files.sources().iter()); - - match PathMatcher::new(exclude_patterns) { - Ok(matcher) => matcher, - Err(error) => { - return Task::ready(Err(anyhow!("invalid exclude pattern: {error}"))); - } - } - }; - - let query = match SearchQuery::regex( - &input.regex, - false, - input.case_sensitive, - false, - false, - include_matcher, - exclude_matcher, - true, // Always match file include pattern against *full project paths* that start with a project root. - None, - ) { - Ok(query) => query, - Err(error) => return Task::ready(Err(error)), - }; - - let results = self - .project - .update(cx, |project, cx| project.search(query, cx)); - - let project = self.project.downgrade(); - cx.spawn(async move |cx| { - futures::pin_mut!(results); - - let mut output = String::new(); - let mut skips_remaining = input.offset; - let mut matches_found = 0; - let mut has_more_matches = false; - - 'outer: while let Some(SearchResult::Buffer { buffer, ranges }) = results.next().await { - if ranges.is_empty() { - continue; - } - - let Ok((Some(path), mut parse_status)) = buffer.read_with(cx, |buffer, cx| { - (buffer.file().map(|file| file.full_path(cx)), buffer.parse_status()) - }) else { - continue; - }; - - // 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) - }) - && 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?; - } - - let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; - - let mut ranges = ranges - .into_iter() - .map(|range| { - let matched = range.to_point(&snapshot); - let matched_end_line_len = snapshot.line_len(matched.end.row); - let full_lines = Point::new(matched.start.row, 0)..Point::new(matched.end.row, matched_end_line_len); - let symbols = snapshot.symbols_containing(matched.start, None); - - if let Some(ancestor_node) = snapshot.syntax_ancestor(full_lines.clone()) { - let full_ancestor_range = ancestor_node.byte_range().to_point(&snapshot); - let end_row = full_ancestor_range.end.row.min(full_ancestor_range.start.row + MAX_ANCESTOR_LINES); - let end_col = snapshot.line_len(end_row); - let capped_ancestor_range = Point::new(full_ancestor_range.start.row, 0)..Point::new(end_row, end_col); - - if capped_ancestor_range.contains_inclusive(&full_lines) { - return (capped_ancestor_range, Some(full_ancestor_range), symbols) - } - } - - let mut matched = matched; - matched.start.column = 0; - matched.start.row = - matched.start.row.saturating_sub(CONTEXT_LINES); - matched.end.row = cmp::min( - snapshot.max_point().row, - matched.end.row + CONTEXT_LINES, - ); - matched.end.column = snapshot.line_len(matched.end.row); - - (matched, None, symbols) - }) - .peekable(); - - let mut file_header_written = false; - - while let Some((mut range, ancestor_range, parent_symbols)) = ranges.next(){ - if skips_remaining > 0 { - skips_remaining -= 1; - continue; - } - - // We'd already found a full page of matches, and we just found one more. - if matches_found >= RESULTS_PER_PAGE { - has_more_matches = true; - break 'outer; - } - - while let Some((next_range, _, _)) = ranges.peek() { - if range.end.row >= next_range.start.row { - range.end = next_range.end; - ranges.next(); - } else { - break; - } - } - - if !file_header_written { - writeln!(output, "\n## Matches in {}", path.display())?; - file_header_written = true; - } - - 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)?; - } - } - - if range.start.row == end_row { - writeln!(output, "L{}", range.start.row + 1)?; - } else { - writeln!(output, "L{}-{}", range.start.row + 1, end_row + 1)?; - } - - output.push_str("```\n"); - output.extend(snapshot.text_for_range(range)); - output.push_str("\n```\n"); - - 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; - } - } - - if matches_found == 0 { - Ok("No matches found".into()) - } else if has_more_matches { - Ok(format!( - "Showing matches {}-{} (there were more matches found; use offset: {} to see next page):\n{output}", - input.offset + 1, - input.offset + matches_found, - input.offset + RESULTS_PER_PAGE, - )) - } else { - Ok(format!("Found {matches_found} matches:\n{output}")) - } - }) - } -} - -#[cfg(test)] -mod tests { - use crate::ToolCallEventStream; - - use super::*; - use gpui::{TestAppContext, UpdateGlobal}; - use language::{Language, LanguageConfig, LanguageMatcher}; - use project::{FakeFs, Project, WorktreeSettings}; - use serde_json::json; - use settings::SettingsStore; - use unindent::Unindent; - use util::path; - - #[gpui::test] - async fn test_grep_tool_with_include_pattern(cx: &mut TestAppContext) { - init_test(cx); - cx.executor().allow_parking(); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root"), - serde_json::json!({ - "src": { - "main.rs": "fn main() {\n println!(\"Hello, world!\");\n}", - "utils": { - "helper.rs": "fn helper() {\n println!(\"I'm a helper!\");\n}", - }, - }, - "tests": { - "test_main.rs": "fn test_main() {\n assert!(true);\n}", - } - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - - // Test with include pattern for Rust files inside the root of the project - let input = GrepToolInput { - regex: "println".to_string(), - include_pattern: Some("root/**/*.rs".to_string()), - offset: 0, - case_sensitive: false, - }; - - let result = run_grep_tool(input, project.clone(), cx).await; - assert!(result.contains("main.rs"), "Should find matches in main.rs"); - assert!( - result.contains("helper.rs"), - "Should find matches in helper.rs" - ); - assert!( - !result.contains("test_main.rs"), - "Should not include test_main.rs even though it's a .rs file (because it doesn't have the pattern)" - ); - - // Test with include pattern for src directory only - let input = GrepToolInput { - regex: "fn".to_string(), - include_pattern: Some("root/**/src/**".to_string()), - offset: 0, - case_sensitive: false, - }; - - let result = run_grep_tool(input, project.clone(), cx).await; - assert!( - result.contains("main.rs"), - "Should find matches in src/main.rs" - ); - assert!( - result.contains("helper.rs"), - "Should find matches in src/utils/helper.rs" - ); - assert!( - !result.contains("test_main.rs"), - "Should not include test_main.rs as it's not in src directory" - ); - - // Test with empty include pattern (should default to all files) - let input = GrepToolInput { - regex: "fn".to_string(), - include_pattern: None, - offset: 0, - case_sensitive: false, - }; - - let result = run_grep_tool(input, project.clone(), cx).await; - assert!(result.contains("main.rs"), "Should find matches in main.rs"); - assert!( - result.contains("helper.rs"), - "Should find matches in helper.rs" - ); - assert!( - result.contains("test_main.rs"), - "Should include test_main.rs" - ); - } - - #[gpui::test] - async fn test_grep_tool_with_case_sensitivity(cx: &mut TestAppContext) { - init_test(cx); - cx.executor().allow_parking(); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root"), - serde_json::json!({ - "case_test.txt": "This file has UPPERCASE and lowercase text.\nUPPERCASE patterns should match only with case_sensitive: true", - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - - // Test case-insensitive search (default) - let input = GrepToolInput { - regex: "uppercase".to_string(), - include_pattern: Some("**/*.txt".to_string()), - offset: 0, - case_sensitive: false, - }; - - let result = run_grep_tool(input, project.clone(), cx).await; - assert!( - result.contains("UPPERCASE"), - "Case-insensitive search should match uppercase" - ); - - // Test case-sensitive search - let input = GrepToolInput { - regex: "uppercase".to_string(), - include_pattern: Some("**/*.txt".to_string()), - offset: 0, - case_sensitive: true, - }; - - let result = run_grep_tool(input, project.clone(), cx).await; - assert!( - !result.contains("UPPERCASE"), - "Case-sensitive search should not match uppercase" - ); - - // Test case-sensitive search - let input = GrepToolInput { - regex: "LOWERCASE".to_string(), - include_pattern: Some("**/*.txt".to_string()), - offset: 0, - case_sensitive: true, - }; - - let result = run_grep_tool(input, project.clone(), cx).await; - - assert!( - !result.contains("lowercase"), - "Case-sensitive search should match lowercase" - ); - - // Test case-sensitive search for lowercase pattern - let input = GrepToolInput { - regex: "lowercase".to_string(), - include_pattern: Some("**/*.txt".to_string()), - offset: 0, - case_sensitive: true, - }; - - let result = run_grep_tool(input, project.clone(), cx).await; - assert!( - result.contains("lowercase"), - "Case-sensitive search should match lowercase text" - ); - } - - /// Helper function to set up a syntax test environment - async fn setup_syntax_test(cx: &mut TestAppContext) -> Entity { - use unindent::Unindent; - init_test(cx); - cx.executor().allow_parking(); - - let fs = FakeFs::new(cx.executor()); - - // Create test file with syntax structures - fs.insert_tree( - path!("/root"), - serde_json::json!({ - "test_syntax.rs": r#" - fn top_level_function() { - println!("This is at the top level"); - } - - mod feature_module { - pub mod nested_module { - pub fn nested_function( - first_arg: String, - second_arg: i32, - ) { - println!("Function in nested module"); - println!("{first_arg}"); - println!("{second_arg}"); - } - } - } - - struct MyStruct { - field1: String, - field2: i32, - } - - impl MyStruct { - fn method_with_block() { - let condition = true; - if condition { - println!("Inside if block"); - } - } - - fn long_function() { - println!("Line 1"); - println!("Line 2"); - println!("Line 3"); - println!("Line 4"); - println!("Line 5"); - println!("Line 6"); - println!("Line 7"); - println!("Line 8"); - println!("Line 9"); - println!("Line 10"); - println!("Line 11"); - println!("Line 12"); - } - } - - trait Processor { - fn process(&self, input: &str) -> String; - } - - impl Processor for MyStruct { - fn process(&self, input: &str) -> String { - format!("Processed: {}", input) - } - } - "#.unindent().trim(), - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - - project.update(cx, |project, _cx| { - project.languages().add(rust_lang().into()) - }); - - project - } - - #[gpui::test] - async fn test_grep_top_level_function(cx: &mut TestAppContext) { - let project = setup_syntax_test(cx).await; - - // Test: Line at the top level of the file - let input = GrepToolInput { - regex: "This is at the top level".to_string(), - include_pattern: Some("**/*.rs".to_string()), - offset: 0, - case_sensitive: false, - }; - - let result = run_grep_tool(input, project.clone(), cx).await; - let expected = r#" - Found 1 matches: - - ## Matches in root/test_syntax.rs - - ### fn top_level_function › L1-3 - ``` - fn top_level_function() { - println!("This is at the top level"); - } - ``` - "# - .unindent(); - assert_eq!(result, expected); - } - - #[gpui::test] - async fn test_grep_function_body(cx: &mut TestAppContext) { - let project = setup_syntax_test(cx).await; - - // Test: Line inside a function body - let input = GrepToolInput { - regex: "Function in nested module".to_string(), - include_pattern: Some("**/*.rs".to_string()), - offset: 0, - case_sensitive: false, - }; - - let result = run_grep_tool(input, project.clone(), cx).await; - let expected = r#" - Found 1 matches: - - ## Matches in root/test_syntax.rs - - ### mod feature_module › pub mod nested_module › pub fn nested_function › L10-14 - ``` - ) { - println!("Function in nested module"); - println!("{first_arg}"); - println!("{second_arg}"); - } - ``` - "# - .unindent(); - assert_eq!(result, expected); - } - - #[gpui::test] - async fn test_grep_function_args_and_body(cx: &mut TestAppContext) { - let project = setup_syntax_test(cx).await; - - // Test: Line with a function argument - let input = GrepToolInput { - regex: "second_arg".to_string(), - include_pattern: Some("**/*.rs".to_string()), - offset: 0, - case_sensitive: false, - }; - - let result = run_grep_tool(input, project.clone(), cx).await; - let expected = r#" - Found 1 matches: - - ## Matches in root/test_syntax.rs - - ### mod feature_module › pub mod nested_module › pub fn nested_function › L7-14 - ``` - pub fn nested_function( - first_arg: String, - second_arg: i32, - ) { - println!("Function in nested module"); - println!("{first_arg}"); - println!("{second_arg}"); - } - ``` - "# - .unindent(); - assert_eq!(result, expected); - } - - #[gpui::test] - async fn test_grep_if_block(cx: &mut TestAppContext) { - use unindent::Unindent; - let project = setup_syntax_test(cx).await; - - // Test: Line inside an if block - let input = GrepToolInput { - regex: "Inside if block".to_string(), - include_pattern: Some("**/*.rs".to_string()), - offset: 0, - case_sensitive: false, - }; - - let result = run_grep_tool(input, project.clone(), cx).await; - let expected = r#" - Found 1 matches: - - ## Matches in root/test_syntax.rs - - ### impl MyStruct › fn method_with_block › L26-28 - ``` - if condition { - println!("Inside if block"); - } - ``` - "# - .unindent(); - assert_eq!(result, expected); - } - - #[gpui::test] - async fn test_grep_long_function_top(cx: &mut TestAppContext) { - use unindent::Unindent; - let project = setup_syntax_test(cx).await; - - // Test: Line in the middle of a long function - should show message about remaining lines - let input = GrepToolInput { - regex: "Line 5".to_string(), - include_pattern: Some("**/*.rs".to_string()), - offset: 0, - case_sensitive: false, - }; - - let result = run_grep_tool(input, project.clone(), cx).await; - let expected = r#" - Found 1 matches: - - ## Matches in root/test_syntax.rs - - ### impl MyStruct › fn long_function › L31-41 - ``` - fn long_function() { - println!("Line 1"); - println!("Line 2"); - println!("Line 3"); - println!("Line 4"); - println!("Line 5"); - println!("Line 6"); - println!("Line 7"); - println!("Line 8"); - println!("Line 9"); - println!("Line 10"); - ``` - - 3 lines remaining in ancestor node. Read the file to see all. - "# - .unindent(); - assert_eq!(result, expected); - } - - #[gpui::test] - async fn test_grep_long_function_bottom(cx: &mut TestAppContext) { - use unindent::Unindent; - let project = setup_syntax_test(cx).await; - - // Test: Line in the long function - let input = GrepToolInput { - regex: "Line 12".to_string(), - include_pattern: Some("**/*.rs".to_string()), - offset: 0, - case_sensitive: false, - }; - - let result = run_grep_tool(input, project.clone(), cx).await; - let expected = r#" - Found 1 matches: - - ## Matches in root/test_syntax.rs - - ### impl MyStruct › fn long_function › L41-45 - ``` - println!("Line 10"); - println!("Line 11"); - println!("Line 12"); - } - } - ``` - "# - .unindent(); - assert_eq!(result, expected); - } - - async fn run_grep_tool( - input: GrepToolInput, - project: Entity, - cx: &mut TestAppContext, - ) -> String { - let tool = Arc::new(GrepTool { project }); - let task = cx.update(|cx| tool.run(input, ToolCallEventStream::test().0, cx)); - - match task.await { - Ok(result) => { - if cfg!(windows) { - result.replace("root\\", "root/") - } else { - result - } - } - Err(e) => panic!("Failed to run grep tool: {}", e), - } - } - - 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); - }); - } - - fn rust_lang() -> Language { - Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_rust::LANGUAGE.into()), - ) - .with_outline_query(include_str!("../../../languages/src/rust/outline.scm")) - .unwrap() - } - - #[gpui::test] - async fn test_grep_security_boundaries(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - - fs.insert_tree( - path!("/"), - json!({ - "project_root": { - "allowed_file.rs": "fn main() { println!(\"This file is in the project\"); }", - ".mysecrets": "SECRET_KEY=abc123\nfn secret() { /* private */ }", - ".secretdir": { - "config": "fn special_configuration() { /* excluded */ }" - }, - ".mymetadata": "fn custom_metadata() { /* excluded */ }", - "subdir": { - "normal_file.rs": "fn normal_file_content() { /* Normal */ }", - "special.privatekey": "fn private_key_content() { /* private */ }", - "data.mysensitive": "fn sensitive_data() { /* private */ }" - } - }, - "outside_project": { - "sensitive_file.rs": "fn outside_function() { /* This file is outside the project */ }" - } - }), - ) - .await; - - cx.update(|cx| { - use gpui::UpdateGlobal; - use project::WorktreeSettings; - use settings::SettingsStore; - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |settings| { - settings.file_scan_exclusions = Some(vec![ - "**/.secretdir".to_string(), - "**/.mymetadata".to_string(), - ]); - settings.private_files = Some(vec![ - "**/.mysecrets".to_string(), - "**/*.privatekey".to_string(), - "**/*.mysensitive".to_string(), - ]); - }); - }); - }); - - let project = Project::test(fs.clone(), [path!("/project_root").as_ref()], cx).await; - - // Searching for files outside the project worktree should return no results - let result = run_grep_tool( - GrepToolInput { - regex: "outside_function".to_string(), - include_pattern: None, - offset: 0, - case_sensitive: false, - }, - project.clone(), - cx, - ) - .await; - let paths = extract_paths_from_results(&result); - assert!( - paths.is_empty(), - "grep_tool should not find files outside the project worktree" - ); - - // Searching within the project should succeed - let result = run_grep_tool( - GrepToolInput { - regex: "main".to_string(), - include_pattern: None, - offset: 0, - case_sensitive: false, - }, - project.clone(), - cx, - ) - .await; - let paths = extract_paths_from_results(&result); - assert!( - paths.iter().any(|p| p.contains("allowed_file.rs")), - "grep_tool should be able to search files inside worktrees" - ); - - // Searching files that match file_scan_exclusions should return no results - let result = run_grep_tool( - GrepToolInput { - regex: "special_configuration".to_string(), - include_pattern: None, - offset: 0, - case_sensitive: false, - }, - project.clone(), - cx, - ) - .await; - let paths = extract_paths_from_results(&result); - assert!( - paths.is_empty(), - "grep_tool should not search files in .secretdir (file_scan_exclusions)" - ); - - let result = run_grep_tool( - GrepToolInput { - regex: "custom_metadata".to_string(), - include_pattern: None, - offset: 0, - case_sensitive: false, - }, - project.clone(), - cx, - ) - .await; - let paths = extract_paths_from_results(&result); - assert!( - paths.is_empty(), - "grep_tool should not search .mymetadata files (file_scan_exclusions)" - ); - - // Searching private files should return no results - let result = run_grep_tool( - GrepToolInput { - regex: "SECRET_KEY".to_string(), - include_pattern: None, - offset: 0, - case_sensitive: false, - }, - project.clone(), - cx, - ) - .await; - let paths = extract_paths_from_results(&result); - assert!( - paths.is_empty(), - "grep_tool should not search .mysecrets (private_files)" - ); - - let result = run_grep_tool( - GrepToolInput { - regex: "private_key_content".to_string(), - include_pattern: None, - offset: 0, - case_sensitive: false, - }, - project.clone(), - cx, - ) - .await; - let paths = extract_paths_from_results(&result); - - assert!( - paths.is_empty(), - "grep_tool should not search .privatekey files (private_files)" - ); - - let result = run_grep_tool( - GrepToolInput { - regex: "sensitive_data".to_string(), - include_pattern: None, - offset: 0, - case_sensitive: false, - }, - project.clone(), - cx, - ) - .await; - let paths = extract_paths_from_results(&result); - assert!( - paths.is_empty(), - "grep_tool should not search .mysensitive files (private_files)" - ); - - // Searching a normal file should still work, even with private_files configured - let result = run_grep_tool( - GrepToolInput { - regex: "normal_file_content".to_string(), - include_pattern: None, - offset: 0, - case_sensitive: false, - }, - project.clone(), - cx, - ) - .await; - let paths = extract_paths_from_results(&result); - assert!( - paths.iter().any(|p| p.contains("normal_file.rs")), - "Should be able to search normal files" - ); - - // Path traversal attempts with .. in include_pattern should not escape project - let result = run_grep_tool( - GrepToolInput { - regex: "outside_function".to_string(), - include_pattern: Some("../outside_project/**/*.rs".to_string()), - offset: 0, - case_sensitive: false, - }, - project.clone(), - cx, - ) - .await; - let paths = extract_paths_from_results(&result); - assert!( - paths.is_empty(), - "grep_tool should not allow escaping project boundaries with relative paths" - ); - } - - #[gpui::test] - async fn test_grep_with_multiple_worktree_settings(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - - // Create first worktree with its own private files - fs.insert_tree( - path!("/worktree1"), - json!({ - ".zed": { - "settings.json": r#"{ - "file_scan_exclusions": ["**/fixture.*"], - "private_files": ["**/secret.rs"] - }"# - }, - "src": { - "main.rs": "fn main() { let secret_key = \"hidden\"; }", - "secret.rs": "const API_KEY: &str = \"secret_value\";", - "utils.rs": "pub fn get_config() -> String { \"config\".to_string() }" - }, - "tests": { - "test.rs": "fn test_secret() { assert!(true); }", - "fixture.sql": "SELECT * FROM secret_table;" - } - }), - ) - .await; - - // Create second worktree with different private files - fs.insert_tree( - path!("/worktree2"), - json!({ - ".zed": { - "settings.json": r#"{ - "file_scan_exclusions": ["**/internal.*"], - "private_files": ["**/private.js", "**/data.json"] - }"# - }, - "lib": { - "public.js": "export function getSecret() { return 'public'; }", - "private.js": "const SECRET_KEY = \"private_value\";", - "data.json": "{\"secret_data\": \"hidden\"}" - }, - "docs": { - "README.md": "# Documentation with secret info", - "internal.md": "Internal secret documentation" - } - }), - ) - .await; - - // Set global settings - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |settings| { - settings.file_scan_exclusions = - Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]); - settings.private_files = Some(vec!["**/.env".to_string()]); - }); - }); - }); - - let project = Project::test( - fs.clone(), - [path!("/worktree1").as_ref(), path!("/worktree2").as_ref()], - cx, - ) - .await; - - // Wait for worktrees to be fully scanned - cx.executor().run_until_parked(); - - // Search for "secret" - should exclude files based on worktree-specific settings - let result = run_grep_tool( - GrepToolInput { - regex: "secret".to_string(), - include_pattern: None, - offset: 0, - case_sensitive: false, - }, - project.clone(), - cx, - ) - .await; - let paths = extract_paths_from_results(&result); - - // Should find matches in non-private files - assert!( - paths.iter().any(|p| p.contains("main.rs")), - "Should find 'secret' in worktree1/src/main.rs" - ); - assert!( - paths.iter().any(|p| p.contains("test.rs")), - "Should find 'secret' in worktree1/tests/test.rs" - ); - assert!( - paths.iter().any(|p| p.contains("public.js")), - "Should find 'secret' in worktree2/lib/public.js" - ); - assert!( - paths.iter().any(|p| p.contains("README.md")), - "Should find 'secret' in worktree2/docs/README.md" - ); - - // Should NOT find matches in private/excluded files based on worktree settings - assert!( - !paths.iter().any(|p| p.contains("secret.rs")), - "Should not search in worktree1/src/secret.rs (local private_files)" - ); - assert!( - !paths.iter().any(|p| p.contains("fixture.sql")), - "Should not search in worktree1/tests/fixture.sql (local file_scan_exclusions)" - ); - assert!( - !paths.iter().any(|p| p.contains("private.js")), - "Should not search in worktree2/lib/private.js (local private_files)" - ); - assert!( - !paths.iter().any(|p| p.contains("data.json")), - "Should not search in worktree2/lib/data.json (local private_files)" - ); - assert!( - !paths.iter().any(|p| p.contains("internal.md")), - "Should not search in worktree2/docs/internal.md (local file_scan_exclusions)" - ); - - // Test with `include_pattern` specific to one worktree - let result = run_grep_tool( - GrepToolInput { - regex: "secret".to_string(), - include_pattern: Some("worktree1/**/*.rs".to_string()), - offset: 0, - case_sensitive: false, - }, - project.clone(), - cx, - ) - .await; - - let paths = extract_paths_from_results(&result); - - // Should only find matches in worktree1 *.rs files (excluding private ones) - assert!( - paths.iter().any(|p| p.contains("main.rs")), - "Should find match in worktree1/src/main.rs" - ); - assert!( - paths.iter().any(|p| p.contains("test.rs")), - "Should find match in worktree1/tests/test.rs" - ); - assert!( - !paths.iter().any(|p| p.contains("secret.rs")), - "Should not find match in excluded worktree1/src/secret.rs" - ); - assert!( - paths.iter().all(|p| !p.contains("worktree2")), - "Should not find any matches in worktree2" - ); - } - - // Helper function to extract file paths from grep results - fn extract_paths_from_results(results: &str) -> Vec { - results - .lines() - .filter(|line| line.starts_with("## Matches in ")) - .map(|line| { - line.strip_prefix("## Matches in ") - .unwrap() - .trim() - .to_string() - }) - .collect() - } -} diff --git a/crates/agent2/src/tools/list_directory_tool.rs b/crates/agent2/src/tools/list_directory_tool.rs deleted file mode 100644 index e6fa8d7431..0000000000 --- a/crates/agent2/src/tools/list_directory_tool.rs +++ /dev/null @@ -1,662 +0,0 @@ -use crate::{AgentTool, ToolCallEventStream}; -use agent_client_protocol::ToolKind; -use anyhow::{Result, anyhow}; -use gpui::{App, Entity, SharedString, Task}; -use project::{Project, WorktreeSettings}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::Settings; -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. -#[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. - /// - /// - /// If the project has the following root directories: - /// - /// - directory1 - /// - directory2 - /// - /// You can list the contents of `directory1` by using the path `directory1`. - /// - /// - /// - /// If the project has the following root directories: - /// - /// - foo - /// - bar - /// - /// If you wanna list contents in the directory `foo/baz`, you should use the path `foo/baz`. - /// - pub path: String, -} - -pub struct ListDirectoryTool { - project: Entity, -} - -impl ListDirectoryTool { - pub fn new(project: Entity) -> Self { - Self { project } - } -} - -impl AgentTool for ListDirectoryTool { - type Input = ListDirectoryToolInput; - type Output = String; - - fn name() -> &'static str { - "list_directory" - } - - fn kind() -> ToolKind { - ToolKind::Read - } - - fn initial_title(&self, input: Result) -> SharedString { - if let Ok(input) = input { - let path = MarkdownInlineCode(&input.path); - format!("List the {path} directory's contents").into() - } else { - "List directory".into() - } - } - - fn run( - self: Arc, - input: Self::Input, - _event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task> { - // Sometimes models will return these even though we tell it to give a path and not a glob. - // When this happens, just list the root worktree directories. - if matches!(input.path.as_str(), "." | "" | "./" | "*") { - let output = self - .project - .read(cx) - .worktrees(cx) - .filter_map(|worktree| { - worktree.read(cx).root_entry().and_then(|entry| { - if entry.is_dir() { - entry.path.to_str() - } else { - None - } - }) - }) - .collect::>() - .join("\n"); - - return Task::ready(Ok(output)); - } - - 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(worktree) = self - .project - .read(cx) - .worktree_for_id(project_path.worktree_id, cx) - else { - return Task::ready(Err(anyhow!("Worktree not found"))); - }; - - // Check if the directory whose contents we're listing is itself excluded or private - let global_settings = WorktreeSettings::get_global(cx); - if global_settings.is_path_excluded(&project_path.path) { - return Task::ready(Err(anyhow!( - "Cannot list directory because its path matches the user's global `file_scan_exclusions` setting: {}", - &input.path - ))); - } - - if global_settings.is_path_private(&project_path.path) { - return Task::ready(Err(anyhow!( - "Cannot list directory because its path matches the user's global `private_files` setting: {}", - &input.path - ))); - } - - let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx); - if worktree_settings.is_path_excluded(&project_path.path) { - return Task::ready(Err(anyhow!( - "Cannot list directory because its path matches the user's worktree`file_scan_exclusions` setting: {}", - &input.path - ))); - } - - if worktree_settings.is_path_private(&project_path.path) { - return Task::ready(Err(anyhow!( - "Cannot list directory because its path matches the user's worktree `private_paths` setting: {}", - &input.path - ))); - } - - let worktree_snapshot = worktree.read(cx).snapshot(); - let worktree_root_name = worktree.read(cx).root_name().to_string(); - - let Some(entry) = worktree_snapshot.entry_for_path(&project_path.path) else { - return Task::ready(Err(anyhow!("Path not found: {}", input.path))); - }; - - if !entry.is_dir() { - return Task::ready(Err(anyhow!("{} is not a directory.", input.path))); - } - let worktree_snapshot = worktree.read(cx).snapshot(); - - let mut folders = Vec::new(); - let mut files = Vec::new(); - - for entry in worktree_snapshot.child_entries(&project_path.path) { - // Skip private and excluded files and directories - if global_settings.is_path_private(&entry.path) - || global_settings.is_path_excluded(&entry.path) - { - continue; - } - - if self - .project - .read(cx) - .find_project_path(&entry.path, cx) - .map(|project_path| { - 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; - } - - let full_path = Path::new(&worktree_root_name) - .join(&entry.path) - .display() - .to_string(); - if entry.is_dir() { - folders.push(full_path); - } else { - files.push(full_path); - } - } - - let mut output = String::new(); - - if !folders.is_empty() { - writeln!(output, "# Folders:\n{}", folders.join("\n")).unwrap(); - } - - if !files.is_empty() { - writeln!(output, "\n# Files:\n{}", files.join("\n")).unwrap(); - } - - if output.is_empty() { - writeln!(output, "{} is empty.", input.path).unwrap(); - } - - Task::ready(Ok(output)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use gpui::{TestAppContext, UpdateGlobal}; - use indoc::indoc; - use project::{FakeFs, Project, WorktreeSettings}; - use serde_json::json; - use settings::SettingsStore; - use util::path; - - fn platform_paths(path_str: &str) -> String { - if cfg!(target_os = "windows") { - path_str.replace("/", "\\") - } else { - path_str.to_string() - } - } - - 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); - }); - } - - #[gpui::test] - async fn test_list_directory_separates_files_and_dirs(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/project"), - json!({ - "src": { - "main.rs": "fn main() {}", - "lib.rs": "pub fn hello() {}", - "models": { - "user.rs": "struct User {}", - "post.rs": "struct Post {}" - }, - "utils": { - "helper.rs": "pub fn help() {}" - } - }, - "tests": { - "integration_test.rs": "#[test] fn test() {}" - }, - "README.md": "# Project", - "Cargo.toml": "[package]" - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - let tool = Arc::new(ListDirectoryTool::new(project)); - - // Test listing root directory - let input = ListDirectoryToolInput { - path: "project".into(), - }; - let output = cx - .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx)) - .await - .unwrap(); - assert_eq!( - output, - platform_paths(indoc! {" - # Folders: - project/src - project/tests - - # Files: - project/Cargo.toml - project/README.md - "}) - ); - - // Test listing src directory - let input = ListDirectoryToolInput { - path: "project/src".into(), - }; - let output = cx - .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx)) - .await - .unwrap(); - assert_eq!( - output, - platform_paths(indoc! {" - # Folders: - project/src/models - project/src/utils - - # Files: - project/src/lib.rs - project/src/main.rs - "}) - ); - - // Test listing directory with only files - let input = ListDirectoryToolInput { - path: "project/tests".into(), - }; - let output = cx - .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx)) - .await - .unwrap(); - assert!(!output.contains("# Folders:")); - assert!(output.contains("# Files:")); - assert!(output.contains(&platform_paths("project/tests/integration_test.rs"))); - } - - #[gpui::test] - async fn test_list_directory_empty_directory(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/project"), - json!({ - "empty_dir": {} - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - let tool = Arc::new(ListDirectoryTool::new(project)); - - let input = ListDirectoryToolInput { - path: "project/empty_dir".into(), - }; - let output = cx - .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx)) - .await - .unwrap(); - assert_eq!(output, "project/empty_dir is empty.\n"); - } - - #[gpui::test] - async fn test_list_directory_error_cases(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/project"), - json!({ - "file.txt": "content" - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - let tool = Arc::new(ListDirectoryTool::new(project)); - - // Test non-existent path - let input = ListDirectoryToolInput { - path: "project/nonexistent".into(), - }; - let output = cx - .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx)) - .await; - assert!(output.unwrap_err().to_string().contains("Path not found")); - - // Test trying to list a file instead of directory - let input = ListDirectoryToolInput { - path: "project/file.txt".into(), - }; - let output = cx - .update(|cx| tool.run(input, ToolCallEventStream::test().0, cx)) - .await; - assert!( - output - .unwrap_err() - .to_string() - .contains("is not a directory") - ); - } - - #[gpui::test] - async fn test_list_directory_security(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/project"), - json!({ - "normal_dir": { - "file1.txt": "content", - "file2.txt": "content" - }, - ".mysecrets": "SECRET_KEY=abc123", - ".secretdir": { - "config": "special configuration", - "secret.txt": "secret content" - }, - ".mymetadata": "custom metadata", - "visible_dir": { - "normal.txt": "normal content", - "special.privatekey": "private key content", - "data.mysensitive": "sensitive data", - ".hidden_subdir": { - "hidden_file.txt": "hidden content" - } - } - }), - ) - .await; - - // Configure settings explicitly - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |settings| { - settings.file_scan_exclusions = Some(vec![ - "**/.secretdir".to_string(), - "**/.mymetadata".to_string(), - "**/.hidden_subdir".to_string(), - ]); - settings.private_files = Some(vec![ - "**/.mysecrets".to_string(), - "**/*.privatekey".to_string(), - "**/*.mysensitive".to_string(), - ]); - }); - }); - }); - - let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - let tool = Arc::new(ListDirectoryTool::new(project)); - - // Listing root directory should exclude private and excluded files - let input = ListDirectoryToolInput { - path: "project".into(), - }; - let output = cx - .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx)) - .await - .unwrap(); - - // Should include normal directories - assert!(output.contains("normal_dir"), "Should list normal_dir"); - assert!(output.contains("visible_dir"), "Should list visible_dir"); - - // Should NOT include excluded or private files - assert!( - !output.contains(".secretdir"), - "Should not list .secretdir (file_scan_exclusions)" - ); - assert!( - !output.contains(".mymetadata"), - "Should not list .mymetadata (file_scan_exclusions)" - ); - assert!( - !output.contains(".mysecrets"), - "Should not list .mysecrets (private_files)" - ); - - // Trying to list an excluded directory should fail - let input = ListDirectoryToolInput { - path: "project/.secretdir".into(), - }; - let output = cx - .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx)) - .await; - assert!( - output - .unwrap_err() - .to_string() - .contains("file_scan_exclusions"), - "Error should mention file_scan_exclusions" - ); - - // Listing a directory should exclude private files within it - let input = ListDirectoryToolInput { - path: "project/visible_dir".into(), - }; - let output = cx - .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx)) - .await - .unwrap(); - - // Should include normal files - assert!(output.contains("normal.txt"), "Should list normal.txt"); - - // Should NOT include private files - assert!( - !output.contains("privatekey"), - "Should not list .privatekey files (private_files)" - ); - assert!( - !output.contains("mysensitive"), - "Should not list .mysensitive files (private_files)" - ); - - // Should NOT include subdirectories that match exclusions - assert!( - !output.contains(".hidden_subdir"), - "Should not list .hidden_subdir (file_scan_exclusions)" - ); - } - - #[gpui::test] - async fn test_list_directory_with_multiple_worktree_settings(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - - // Create first worktree with its own private files - fs.insert_tree( - path!("/worktree1"), - json!({ - ".zed": { - "settings.json": r#"{ - "file_scan_exclusions": ["**/fixture.*"], - "private_files": ["**/secret.rs", "**/config.toml"] - }"# - }, - "src": { - "main.rs": "fn main() { println!(\"Hello from worktree1\"); }", - "secret.rs": "const API_KEY: &str = \"secret_key_1\";", - "config.toml": "[database]\nurl = \"postgres://localhost/db1\"" - }, - "tests": { - "test.rs": "mod tests { fn test_it() {} }", - "fixture.sql": "CREATE TABLE users (id INT, name VARCHAR(255));" - } - }), - ) - .await; - - // Create second worktree with different private files - fs.insert_tree( - path!("/worktree2"), - json!({ - ".zed": { - "settings.json": r#"{ - "file_scan_exclusions": ["**/internal.*"], - "private_files": ["**/private.js", "**/data.json"] - }"# - }, - "lib": { - "public.js": "export function greet() { return 'Hello from worktree2'; }", - "private.js": "const SECRET_TOKEN = \"private_token_2\";", - "data.json": "{\"api_key\": \"json_secret_key\"}" - }, - "docs": { - "README.md": "# Public Documentation", - "internal.md": "# Internal Secrets and Configuration" - } - }), - ) - .await; - - // Set global settings - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |settings| { - settings.file_scan_exclusions = - Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]); - settings.private_files = Some(vec!["**/.env".to_string()]); - }); - }); - }); - - let project = Project::test( - fs.clone(), - [path!("/worktree1").as_ref(), path!("/worktree2").as_ref()], - cx, - ) - .await; - - // Wait for worktrees to be fully scanned - cx.executor().run_until_parked(); - - let tool = Arc::new(ListDirectoryTool::new(project)); - - // Test listing worktree1/src - should exclude secret.rs and config.toml based on local settings - let input = ListDirectoryToolInput { - path: "worktree1/src".into(), - }; - let output = cx - .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx)) - .await - .unwrap(); - assert!(output.contains("main.rs"), "Should list main.rs"); - assert!( - !output.contains("secret.rs"), - "Should not list secret.rs (local private_files)" - ); - assert!( - !output.contains("config.toml"), - "Should not list config.toml (local private_files)" - ); - - // Test listing worktree1/tests - should exclude fixture.sql based on local settings - let input = ListDirectoryToolInput { - path: "worktree1/tests".into(), - }; - let output = cx - .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx)) - .await - .unwrap(); - assert!(output.contains("test.rs"), "Should list test.rs"); - assert!( - !output.contains("fixture.sql"), - "Should not list fixture.sql (local file_scan_exclusions)" - ); - - // Test listing worktree2/lib - should exclude private.js and data.json based on local settings - let input = ListDirectoryToolInput { - path: "worktree2/lib".into(), - }; - let output = cx - .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx)) - .await - .unwrap(); - assert!(output.contains("public.js"), "Should list public.js"); - assert!( - !output.contains("private.js"), - "Should not list private.js (local private_files)" - ); - assert!( - !output.contains("data.json"), - "Should not list data.json (local private_files)" - ); - - // Test listing worktree2/docs - should exclude internal.md based on local settings - let input = ListDirectoryToolInput { - path: "worktree2/docs".into(), - }; - let output = cx - .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx)) - .await - .unwrap(); - assert!(output.contains("README.md"), "Should list README.md"); - assert!( - !output.contains("internal.md"), - "Should not list internal.md (local file_scan_exclusions)" - ); - - // Test trying to list an excluded directory directly - let input = ListDirectoryToolInput { - path: "worktree1/src/secret.rs".into(), - }; - let output = cx - .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx)) - .await; - assert!( - output - .unwrap_err() - .to_string() - .contains("Cannot list directory"), - ); - } -} diff --git a/crates/agent2/src/tools/move_path_tool.rs b/crates/agent2/src/tools/move_path_tool.rs deleted file mode 100644 index d9fb60651b..0000000000 --- a/crates/agent2/src/tools/move_path_tool.rs +++ /dev/null @@ -1,120 +0,0 @@ -use crate::{AgentTool, ToolCallEventStream}; -use agent_client_protocol::ToolKind; -use anyhow::{Context as _, Result, anyhow}; -use gpui::{App, AppContext, Entity, SharedString, Task}; -use project::Project; -use schemars::JsonSchema; -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. -/// -/// 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. -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct MovePathToolInput { - /// The source path of the file or directory to move/rename. - /// - /// - /// If the project has the following files: - /// - /// - directory1/a/something.txt - /// - directory2/a/things.txt - /// - directory3/a/other.txt - /// - /// You can move the first file by providing a source_path of "directory1/a/something.txt" - /// - pub source_path: String, - - /// The destination path where the file or directory should be moved/renamed to. - /// If the paths are the same except for the filename, then this will be a rename. - /// - /// - /// To move "directory1/a/something.txt" to "directory2/b/renamed.txt", - /// provide a destination_path of "directory2/b/renamed.txt" - /// - pub destination_path: String, -} - -pub struct MovePathTool { - project: Entity, -} - -impl MovePathTool { - pub fn new(project: Entity) -> Self { - Self { project } - } -} - -impl AgentTool for MovePathTool { - type Input = MovePathToolInput; - type Output = String; - - fn name() -> &'static str { - "move_path" - } - - fn kind() -> ToolKind { - ToolKind::Move - } - - fn initial_title(&self, input: Result) -> SharedString { - if let Ok(input) = input { - let src = MarkdownInlineCode(&input.source_path); - let dest = MarkdownInlineCode(&input.destination_path); - let src_path = Path::new(&input.source_path); - let dest_path = Path::new(&input.destination_path); - - match dest_path - .file_name() - .and_then(|os_str| os_str.to_os_string().into_string().ok()) - { - Some(filename) if src_path.parent() == dest_path.parent() => { - let filename = MarkdownInlineCode(&filename); - format!("Rename {src} to {filename}").into() - } - _ => format!("Move {src} to {dest}").into(), - } - } else { - "Move path".into() - } - } - - fn run( - self: Arc, - input: Self::Input, - _event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task> { - let rename_task = self.project.update(cx, |project, cx| { - match project - .find_project_path(&input.source_path, cx) - .and_then(|project_path| project.entry_for_path(&project_path, cx)) - { - Some(entity) => match project.find_project_path(&input.destination_path, cx) { - Some(project_path) => project.rename_entry(entity.id, project_path.path, cx), - None => Task::ready(Err(anyhow!( - "Destination path {} was outside the project.", - input.destination_path - ))), - }, - None => Task::ready(Err(anyhow!( - "Source path {} was not found in the project.", - input.source_path - ))), - } - }); - - cx.background_spawn(async move { - let _ = rename_task.await.with_context(|| { - format!("Moving {} to {}", input.source_path, input.destination_path) - })?; - Ok(format!( - "Moved {} to {}", - input.source_path, input.destination_path - )) - }) - } -} diff --git a/crates/agent2/src/tools/now_tool.rs b/crates/agent2/src/tools/now_tool.rs deleted file mode 100644 index 9467e7db68..0000000000 --- a/crates/agent2/src/tools/now_tool.rs +++ /dev/null @@ -1,59 +0,0 @@ -use std::sync::Arc; - -use agent_client_protocol as acp; -use anyhow::Result; -use chrono::{Local, Utc}; -use gpui::{App, SharedString, Task}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -use crate::{AgentTool, ToolCallEventStream}; - -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum Timezone { - /// Use UTC for the datetime. - Utc, - /// Use local time for the datetime. - Local, -} - -/// Returns the current datetime in RFC 3339 format. -/// Only use this tool when the user specifically asks for it or the current task would benefit from knowing the current datetime. -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct NowToolInput { - /// The timezone to use for the datetime. - timezone: Timezone, -} - -pub struct NowTool; - -impl AgentTool for NowTool { - type Input = NowToolInput; - type Output = String; - - fn name() -> &'static str { - "now" - } - - fn kind() -> acp::ToolKind { - acp::ToolKind::Other - } - - fn initial_title(&self, _input: Result) -> SharedString { - "Get current time".into() - } - - fn run( - self: Arc, - input: Self::Input, - _event_stream: ToolCallEventStream, - _cx: &mut App, - ) -> Task> { - let now = match input.timezone { - Timezone::Utc => Utc::now().to_rfc3339(), - Timezone::Local => Local::now().to_rfc3339(), - }; - Task::ready(Ok(format!("The current datetime is {now}."))) - } -} diff --git a/crates/agent2/src/tools/open_tool.rs b/crates/agent2/src/tools/open_tool.rs deleted file mode 100644 index df7b04c787..0000000000 --- a/crates/agent2/src/tools/open_tool.rs +++ /dev/null @@ -1,166 +0,0 @@ -use crate::AgentTool; -use agent_client_protocol::ToolKind; -use anyhow::{Context as _, Result}; -use gpui::{App, AppContext, Entity, SharedString, Task}; -use project::Project; -use schemars::JsonSchema; -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: -/// -/// - 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. -/// -/// 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. - path_or_url: String, -} - -pub struct OpenTool { - project: Entity, -} - -impl OpenTool { - pub fn new(project: Entity) -> Self { - Self { project } - } -} - -impl AgentTool for OpenTool { - type Input = OpenToolInput; - type Output = String; - - fn name() -> &'static str { - "open" - } - - fn kind() -> ToolKind { - ToolKind::Execute - } - - fn initial_title(&self, input: Result) -> SharedString { - if let Ok(input) = input { - format!("Open `{}`", MarkdownEscaped(&input.path_or_url)).into() - } else { - "Open file or URL".into() - } - } - - fn run( - self: Arc, - input: Self::Input, - event_stream: crate::ToolCallEventStream, - cx: &mut App, - ) -> Task> { - // 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); - cx.background_spawn(async move { - authorize.await?; - - match abs_path { - Some(path) => open::that(path), - None => open::that(&input.path_or_url), - } - .context("Failed to open URL or file path")?; - - Ok(format!("Successfully opened {}", input.path_or_url)) - }) - } -} - -fn to_absolute_path( - potential_path: &str, - project: Entity, - cx: &mut App, -) -> Option { - let project = project.read(cx); - project - .find_project_path(PathBuf::from(potential_path), cx) - .and_then(|project_path| project.absolute_path(&project_path, cx)) -} - -#[cfg(test)] -mod tests { - use super::*; - use gpui::TestAppContext; - use project::{FakeFs, Project}; - use settings::SettingsStore; - use std::path::Path; - use tempfile::TempDir; - - #[gpui::test] - async fn test_to_absolute_path(cx: &mut TestAppContext) { - init_test(cx); - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let temp_path = temp_dir.path().to_string_lossy().to_string(); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - &temp_path, - serde_json::json!({ - "src": { - "main.rs": "fn main() {}", - "lib.rs": "pub fn lib_fn() {}" - }, - "docs": { - "readme.md": "# Project Documentation" - } - }), - ) - .await; - - // Use the temp_path as the root directory, not just its filename - let project = Project::test(fs.clone(), [temp_dir.path()], cx).await; - - // Test cases where the function should return Some - cx.update(|cx| { - // Project-relative paths should return Some - // Create paths using the last segment of the temp path to simulate a project-relative path - let root_dir_name = Path::new(&temp_path) - .file_name() - .unwrap_or_else(|| std::ffi::OsStr::new("temp")) - .to_string_lossy(); - - assert!( - to_absolute_path(&format!("{root_dir_name}/src/main.rs"), project.clone(), cx) - .is_some(), - "Failed to resolve main.rs path" - ); - - assert!( - to_absolute_path( - &format!("{root_dir_name}/docs/readme.md",), - project.clone(), - cx, - ) - .is_some(), - "Failed to resolve readme.md path" - ); - - // External URL should return None - let result = to_absolute_path("https://example.com", project.clone(), cx); - assert_eq!(result, None, "External URLs should return None"); - - // Path outside project - let result = to_absolute_path("../invalid/path", project.clone(), cx); - assert_eq!(result, None, "Paths outside the project should return None"); - }); - } - - 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); - }); - } -} diff --git a/crates/agent2/src/tools/read_file_tool.rs b/crates/agent2/src/tools/read_file_tool.rs deleted file mode 100644 index e771c26eca..0000000000 --- a/crates/agent2/src/tools/read_file_tool.rs +++ /dev/null @@ -1,950 +0,0 @@ -use action_log::ActionLog; -use agent_client_protocol::{self as acp, ToolCallUpdateFields}; -use anyhow::{Context as _, Result, anyhow}; -use assistant_tool::outline; -use gpui::{App, Entity, SharedString, Task}; -use indoc::formatdoc; -use language::Point; -use language_model::{LanguageModelImage, LanguageModelToolResultContent}; -use project::{AgentLocation, ImageItem, Project, WorktreeSettings, image_store}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::Settings; -use std::{path::Path, sync::Arc}; -use util::markdown::MarkdownCodeBlock; - -use crate::{AgentTool, ToolCallEventStream}; - -/// Reads the content of the given file in the project. -/// -/// - Never attempt to read a path that hasn't been previously mentioned. -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -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. - /// - /// - /// If the project has the following root directories: - /// - /// - /a/b/directory1 - /// - /c/d/directory2 - /// - /// If you want to access `file.txt` in `directory1`, you should use the path `directory1/file.txt`. - /// If you want to access `file.txt` in `directory2`, you should use the path `directory2/file.txt`. - /// - pub path: String, - /// Optional line number to start reading on (1-based index) - #[serde(default)] - pub start_line: Option, - /// Optional line number to end reading on (1-based index, inclusive) - #[serde(default)] - pub end_line: Option, -} - -pub struct ReadFileTool { - project: Entity, - action_log: Entity, -} - -impl ReadFileTool { - pub fn new(project: Entity, action_log: Entity) -> Self { - Self { - project, - action_log, - } - } -} - -impl AgentTool for ReadFileTool { - type Input = ReadFileToolInput; - type Output = LanguageModelToolResultContent; - - fn name() -> &'static str { - "read_file" - } - - fn kind() -> acp::ToolKind { - acp::ToolKind::Read - } - - fn initial_title(&self, input: Result) -> SharedString { - input - .ok() - .as_ref() - .and_then(|input| Path::new(&input.path).file_name()) - .map(|file_name| file_name.to_string_lossy().to_string().into()) - .unwrap_or_default() - } - - fn run( - self: Arc, - input: Self::Input, - event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task> { - 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))); - }; - - // Error out if this path is either excluded or private in global settings - let global_settings = WorktreeSettings::get_global(cx); - if global_settings.is_path_excluded(&project_path.path) { - return Task::ready(Err(anyhow!( - "Cannot read file because its path matches the global `file_scan_exclusions` setting: {}", - &input.path - ))); - } - - if global_settings.is_path_private(&project_path.path) { - return Task::ready(Err(anyhow!( - "Cannot read file because its path matches the global `private_files` setting: {}", - &input.path - ))); - } - - // Error out if this path is either excluded or private in worktree settings - let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx); - if worktree_settings.is_path_excluded(&project_path.path) { - return Task::ready(Err(anyhow!( - "Cannot read file because its path matches the worktree `file_scan_exclusions` setting: {}", - &input.path - ))); - } - - if worktree_settings.is_path_private(&project_path.path) { - return Task::ready(Err(anyhow!( - "Cannot read file because its path matches the worktree `private_files` setting: {}", - &input.path - ))); - } - - let file_path = input.path.clone(); - - if image_store::is_image_file(&self.project, &project_path, cx) { - return cx.spawn(async move |cx| { - let image_entity: Entity = cx - .update(|cx| { - self.project.update(cx, |project, cx| { - project.open_image(project_path.clone(), cx) - }) - })? - .await?; - - let image = - image_entity.read_with(cx, |image_item, _| Arc::clone(&image_item.image))?; - - let language_model_image = cx - .update(|cx| LanguageModelImage::from_image(image, cx))? - .await - .context("processing image")?; - - Ok(language_model_image.into()) - }); - } - - let project = self.project.clone(); - let action_log = self.action_log.clone(); - - cx.spawn(async move |cx| { - let buffer = cx - .update(|cx| { - project.update(cx, |project, cx| { - project.open_buffer(project_path.clone(), cx) - }) - })? - .await?; - if buffer.read_with(cx, |buffer, _| { - buffer - .file() - .as_ref() - .is_none_or(|file| !file.disk_state().exists()) - })? { - anyhow::bail!("{file_path} not found"); - } - - let mut anchor = None; - - // Check if specific line ranges are provided - let result = if input.start_line.is_some() || input.end_line.is_some() { - let result = buffer.read_with(cx, |buffer, _cx| { - let text = buffer.text(); - // .max(1) because despite instructions to be 1-indexed, sometimes the model passes 0. - let start = input.start_line.unwrap_or(1).max(1); - let start_row = start - 1; - if start_row <= buffer.max_point().row { - let column = buffer.line_indent_for_row(start_row).raw_len(); - anchor = Some(buffer.anchor_before(Point::new(start_row, column))); - } - - let lines = text.split('\n').skip(start_row as usize); - if let Some(end) = input.end_line { - let count = end.saturating_sub(start).saturating_add(1); // Ensure at least 1 line - itertools::intersperse(lines.take(count as usize), "\n").collect::() - } else { - itertools::intersperse(lines, "\n").collect::() - } - })?; - - action_log.update(cx, |log, cx| { - log.buffer_read(buffer.clone(), cx); - })?; - - Ok(result.into()) - } else { - // No line ranges specified, so check file size to see if it's too big. - let file_size = buffer.read_with(cx, |buffer, _cx| buffer.text().len())?; - - if file_size <= outline::AUTO_OUTLINE_SIZE { - // File is small enough, so return its contents. - let result = buffer.read_with(cx, |buffer, _cx| buffer.text())?; - - action_log.update(cx, |log, cx| { - log.buffer_read(buffer.clone(), cx); - })?; - - Ok(result.into()) - } else { - // File is too big, so return the outline - // and a suggestion to read again with line numbers. - let outline = - outline::file_outline(project.clone(), file_path, action_log, None, cx) - .await?; - Ok(formatdoc! {" - This file was too big to read all at once. - - Here is an outline of its symbols: - - {outline} - - Using the line numbers in this outline, you can call this tool again - while specifying the start_line and end_line fields to see the - implementations of symbols in the outline. - - Alternatively, you can fall back to the `grep` tool (if available) - to search the file for specific content." - } - .into()) - } - }; - - 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, - ); - 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 let Ok(LanguageModelToolResultContent::Text(text)) = &result { - let markdown = MarkdownCodeBlock { - tag: &input.path, - text, - } - .to_string(); - event_stream.update_fields(ToolCallUpdateFields { - content: Some(vec![acp::ToolCallContent::Content { - content: markdown.into(), - }]), - ..Default::default() - }) - } - } - })?; - - result - }) - } -} - -#[cfg(test)] -mod test { - use super::*; - use gpui::{AppContext, TestAppContext, UpdateGlobal as _}; - use language::{Language, LanguageConfig, LanguageMatcher, tree_sitter_rust}; - use project::{FakeFs, Project}; - use serde_json::json; - use settings::SettingsStore; - use util::path; - - #[gpui::test] - async fn test_read_nonexistent_file(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/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 tool = Arc::new(ReadFileTool::new(project, action_log)); - let (event_stream, _) = ToolCallEventStream::test(); - - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "root/nonexistent_file.txt".to_string(), - start_line: None, - end_line: None, - }; - tool.run(input, event_stream, cx) - }) - .await; - assert_eq!( - result.unwrap_err().to_string(), - "root/nonexistent_file.txt not found" - ); - } - - #[gpui::test] - async fn test_read_small_file(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root"), - json!({ - "small_file.txt": "This is a small file content" - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let tool = Arc::new(ReadFileTool::new(project, action_log)); - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "root/small_file.txt".into(), - start_line: None, - end_line: None, - }; - tool.run(input, ToolCallEventStream::test().0, cx) - }) - .await; - assert_eq!(result.unwrap(), "This is a small file content".into()); - } - - #[gpui::test] - async fn test_read_large_file(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root"), - json!({ - "large_file.rs": (0..1000).map(|i| format!("struct Test{} {{\n a: u32,\n b: usize,\n}}", i)).collect::>().join("\n") - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let language_registry = project.read_with(cx, |project, _| project.languages().clone()); - language_registry.add(Arc::new(rust_lang())); - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let tool = Arc::new(ReadFileTool::new(project, action_log)); - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "root/large_file.rs".into(), - start_line: None, - end_line: None, - }; - tool.clone().run(input, ToolCallEventStream::test().0, cx) - }) - .await - .unwrap(); - let content = result.to_str().unwrap(); - - assert_eq!( - content.lines().skip(4).take(6).collect::>(), - vec![ - "struct Test0 [L1-4]", - " a [L2]", - " b [L3]", - "struct Test1 [L5-8]", - " a [L6]", - " b [L7]", - ] - ); - - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "root/large_file.rs".into(), - start_line: None, - end_line: None, - }; - tool.run(input, ToolCallEventStream::test().0, cx) - }) - .await - .unwrap(); - let content = result.to_str().unwrap(); - let expected_content = (0..1000) - .flat_map(|i| { - vec![ - format!("struct Test{} [L{}-{}]", i, i * 4 + 1, i * 4 + 4), - format!(" a [L{}]", i * 4 + 2), - format!(" b [L{}]", i * 4 + 3), - ] - }) - .collect::>(); - pretty_assertions::assert_eq!( - content - .lines() - .skip(4) - .take(expected_content.len()) - .collect::>(), - expected_content - ); - } - - #[gpui::test] - async fn test_read_file_with_line_range(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root"), - json!({ - "multiline.txt": "Line 1\nLine 2\nLine 3\nLine 4\nLine 5" - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let tool = Arc::new(ReadFileTool::new(project, action_log)); - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "root/multiline.txt".to_string(), - start_line: Some(2), - end_line: Some(4), - }; - tool.run(input, ToolCallEventStream::test().0, cx) - }) - .await; - assert_eq!(result.unwrap(), "Line 2\nLine 3\nLine 4".into()); - } - - #[gpui::test] - async fn test_read_file_line_range_edge_cases(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root"), - json!({ - "multiline.txt": "Line 1\nLine 2\nLine 3\nLine 4\nLine 5" - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let tool = Arc::new(ReadFileTool::new(project, action_log)); - - // start_line of 0 should be treated as 1 - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "root/multiline.txt".to_string(), - start_line: Some(0), - end_line: Some(2), - }; - tool.clone().run(input, ToolCallEventStream::test().0, cx) - }) - .await; - assert_eq!(result.unwrap(), "Line 1\nLine 2".into()); - - // end_line of 0 should result in at least 1 line - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "root/multiline.txt".to_string(), - start_line: Some(1), - end_line: Some(0), - }; - tool.clone().run(input, ToolCallEventStream::test().0, cx) - }) - .await; - assert_eq!(result.unwrap(), "Line 1".into()); - - // when start_line > end_line, should still return at least 1 line - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "root/multiline.txt".to_string(), - start_line: Some(3), - end_line: Some(2), - }; - tool.clone().run(input, ToolCallEventStream::test().0, cx) - }) - .await; - assert_eq!(result.unwrap(), "Line 3".into()); - } - - 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); - }); - } - - fn rust_lang() -> Language { - Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_rust::LANGUAGE.into()), - ) - .with_outline_query( - r#" - (line_comment) @annotation - - (struct_item - "struct" @context - name: (_) @name) @item - (enum_item - "enum" @context - name: (_) @name) @item - (enum_variant - name: (_) @name) @item - (field_declaration - name: (_) @name) @item - (impl_item - "impl" @context - trait: (_)? @name - "for"? @context - type: (_) @name - body: (_ "{" (_)* "}")) @item - (function_item - "fn" @context - name: (_) @name) @item - (mod_item - "mod" @context - name: (_) @name) @item - "#, - ) - .unwrap() - } - - #[gpui::test] - async fn test_read_file_security(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - - fs.insert_tree( - path!("/"), - json!({ - "project_root": { - "allowed_file.txt": "This file is in the project", - ".mysecrets": "SECRET_KEY=abc123", - ".secretdir": { - "config": "special configuration" - }, - ".mymetadata": "custom metadata", - "subdir": { - "normal_file.txt": "Normal file content", - "special.privatekey": "private key content", - "data.mysensitive": "sensitive data" - } - }, - "outside_project": { - "sensitive_file.txt": "This file is outside the project" - } - }), - ) - .await; - - cx.update(|cx| { - use gpui::UpdateGlobal; - use project::WorktreeSettings; - use settings::SettingsStore; - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |settings| { - settings.file_scan_exclusions = Some(vec![ - "**/.secretdir".to_string(), - "**/.mymetadata".to_string(), - ]); - settings.private_files = Some(vec![ - "**/.mysecrets".to_string(), - "**/*.privatekey".to_string(), - "**/*.mysensitive".to_string(), - ]); - }); - }); - }); - - let project = Project::test(fs.clone(), [path!("/project_root").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let tool = Arc::new(ReadFileTool::new(project, action_log)); - - // Reading a file outside the project worktree should fail - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "/outside_project/sensitive_file.txt".to_string(), - start_line: None, - end_line: None, - }; - tool.clone().run(input, ToolCallEventStream::test().0, cx) - }) - .await; - assert!( - result.is_err(), - "read_file_tool should error when attempting to read an absolute path outside a worktree" - ); - - // Reading a file within the project should succeed - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "project_root/allowed_file.txt".to_string(), - start_line: None, - end_line: None, - }; - tool.clone().run(input, ToolCallEventStream::test().0, cx) - }) - .await; - assert!( - result.is_ok(), - "read_file_tool should be able to read files inside worktrees" - ); - - // Reading files that match file_scan_exclusions should fail - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "project_root/.secretdir/config".to_string(), - start_line: None, - end_line: None, - }; - tool.clone().run(input, ToolCallEventStream::test().0, cx) - }) - .await; - assert!( - result.is_err(), - "read_file_tool should error when attempting to read files in .secretdir (file_scan_exclusions)" - ); - - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "project_root/.mymetadata".to_string(), - start_line: None, - end_line: None, - }; - tool.clone().run(input, ToolCallEventStream::test().0, cx) - }) - .await; - assert!( - result.is_err(), - "read_file_tool should error when attempting to read .mymetadata files (file_scan_exclusions)" - ); - - // Reading private files should fail - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "project_root/.mysecrets".to_string(), - start_line: None, - end_line: None, - }; - tool.clone().run(input, ToolCallEventStream::test().0, cx) - }) - .await; - assert!( - result.is_err(), - "read_file_tool should error when attempting to read .mysecrets (private_files)" - ); - - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "project_root/subdir/special.privatekey".to_string(), - start_line: None, - end_line: None, - }; - tool.clone().run(input, ToolCallEventStream::test().0, cx) - }) - .await; - assert!( - result.is_err(), - "read_file_tool should error when attempting to read .privatekey files (private_files)" - ); - - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "project_root/subdir/data.mysensitive".to_string(), - start_line: None, - end_line: None, - }; - tool.clone().run(input, ToolCallEventStream::test().0, cx) - }) - .await; - assert!( - result.is_err(), - "read_file_tool should error when attempting to read .mysensitive files (private_files)" - ); - - // Reading a normal file should still work, even with private_files configured - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "project_root/subdir/normal_file.txt".to_string(), - start_line: None, - end_line: None, - }; - tool.clone().run(input, ToolCallEventStream::test().0, cx) - }) - .await; - assert!(result.is_ok(), "Should be able to read normal files"); - assert_eq!(result.unwrap(), "Normal file content".into()); - - // Path traversal attempts with .. should fail - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "project_root/../outside_project/sensitive_file.txt".to_string(), - start_line: None, - end_line: None, - }; - tool.run(input, ToolCallEventStream::test().0, cx) - }) - .await; - assert!( - result.is_err(), - "read_file_tool should error when attempting to read a relative path that resolves to outside a worktree" - ); - } - - #[gpui::test] - async fn test_read_file_with_multiple_worktree_settings(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - - // Create first worktree with its own private_files setting - fs.insert_tree( - path!("/worktree1"), - json!({ - "src": { - "main.rs": "fn main() { println!(\"Hello from worktree1\"); }", - "secret.rs": "const API_KEY: &str = \"secret_key_1\";", - "config.toml": "[database]\nurl = \"postgres://localhost/db1\"" - }, - "tests": { - "test.rs": "mod tests { fn test_it() {} }", - "fixture.sql": "CREATE TABLE users (id INT, name VARCHAR(255));" - }, - ".zed": { - "settings.json": r#"{ - "file_scan_exclusions": ["**/fixture.*"], - "private_files": ["**/secret.rs", "**/config.toml"] - }"# - } - }), - ) - .await; - - // Create second worktree with different private_files setting - fs.insert_tree( - path!("/worktree2"), - json!({ - "lib": { - "public.js": "export function greet() { return 'Hello from worktree2'; }", - "private.js": "const SECRET_TOKEN = \"private_token_2\";", - "data.json": "{\"api_key\": \"json_secret_key\"}" - }, - "docs": { - "README.md": "# Public Documentation", - "internal.md": "# Internal Secrets and Configuration" - }, - ".zed": { - "settings.json": r#"{ - "file_scan_exclusions": ["**/internal.*"], - "private_files": ["**/private.js", "**/data.json"] - }"# - } - }), - ) - .await; - - // Set global settings - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings::(cx, |settings| { - settings.file_scan_exclusions = - Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]); - settings.private_files = Some(vec!["**/.env".to_string()]); - }); - }); - }); - - let project = Project::test( - fs.clone(), - [path!("/worktree1").as_ref(), path!("/worktree2").as_ref()], - cx, - ) - .await; - - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let tool = Arc::new(ReadFileTool::new(project.clone(), action_log.clone())); - - // Test reading allowed files in worktree1 - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "worktree1/src/main.rs".to_string(), - start_line: None, - end_line: None, - }; - tool.clone().run(input, ToolCallEventStream::test().0, cx) - }) - .await - .unwrap(); - - assert_eq!( - result, - "fn main() { println!(\"Hello from worktree1\"); }".into() - ); - - // Test reading private file in worktree1 should fail - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "worktree1/src/secret.rs".to_string(), - start_line: None, - end_line: None, - }; - tool.clone().run(input, ToolCallEventStream::test().0, cx) - }) - .await; - - assert!(result.is_err()); - assert!( - result - .unwrap_err() - .to_string() - .contains("worktree `private_files` setting"), - "Error should mention worktree private_files setting" - ); - - // Test reading excluded file in worktree1 should fail - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "worktree1/tests/fixture.sql".to_string(), - start_line: None, - end_line: None, - }; - tool.clone().run(input, ToolCallEventStream::test().0, cx) - }) - .await; - - assert!(result.is_err()); - assert!( - result - .unwrap_err() - .to_string() - .contains("worktree `file_scan_exclusions` setting"), - "Error should mention worktree file_scan_exclusions setting" - ); - - // Test reading allowed files in worktree2 - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "worktree2/lib/public.js".to_string(), - start_line: None, - end_line: None, - }; - tool.clone().run(input, ToolCallEventStream::test().0, cx) - }) - .await - .unwrap(); - - assert_eq!( - result, - "export function greet() { return 'Hello from worktree2'; }".into() - ); - - // Test reading private file in worktree2 should fail - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "worktree2/lib/private.js".to_string(), - start_line: None, - end_line: None, - }; - tool.clone().run(input, ToolCallEventStream::test().0, cx) - }) - .await; - - assert!(result.is_err()); - assert!( - result - .unwrap_err() - .to_string() - .contains("worktree `private_files` setting"), - "Error should mention worktree private_files setting" - ); - - // Test reading excluded file in worktree2 should fail - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "worktree2/docs/internal.md".to_string(), - start_line: None, - end_line: None, - }; - tool.clone().run(input, ToolCallEventStream::test().0, cx) - }) - .await; - - assert!(result.is_err()); - assert!( - result - .unwrap_err() - .to_string() - .contains("worktree `file_scan_exclusions` setting"), - "Error should mention worktree file_scan_exclusions setting" - ); - - // Test that files allowed in one worktree but not in another are handled correctly - // (e.g., config.toml is private in worktree1 but doesn't exist in worktree2) - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "worktree1/src/config.toml".to_string(), - start_line: None, - end_line: None, - }; - tool.clone().run(input, ToolCallEventStream::test().0, cx) - }) - .await; - - assert!(result.is_err()); - assert!( - result - .unwrap_err() - .to_string() - .contains("worktree `private_files` setting"), - "Config.toml should be blocked by worktree1's private_files setting" - ); - } -} diff --git a/crates/agent2/src/tools/terminal_tool.rs b/crates/agent2/src/tools/terminal_tool.rs deleted file mode 100644 index f41b909d0b..0000000000 --- a/crates/agent2/src/tools/terminal_tool.rs +++ /dev/null @@ -1,468 +0,0 @@ -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 schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::{ - path::{Path, PathBuf}, - sync::Arc, -}; -use util::{ResultExt, get_system_shell, markdown::MarkdownInlineCode}; - -use crate::{AgentTool, ToolCallEventStream}; - -const COMMAND_OUTPUT_LIMIT: usize = 16 * 1024; - -/// Executes a shell one-liner and returns the combined output. -/// -/// This tool spawns a process using the user's shell, reads from stdout and stderr (preserving the order of writes), and returns a string with the combined output result. -/// -/// The output results will be shown to the user already, only list it again if necessary, avoid being redundant. -/// -/// Make sure you use the `cd` parameter to navigate to one of the root directories of the project. NEVER do it as part of the `command` itself, otherwise it will error. -/// -/// Do not use this tool for commands that run indefinitely, such as servers (like `npm run start`, `npm run dev`, `python -m http.server`, etc) or file watchers that don't terminate on their own. -/// -/// Remember that each invocation of this tool will spawn a new shell process, so you can't rely on any state from previous invocations. -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] -pub struct TerminalToolInput { - /// The one-liner command to execute. - command: String, - /// Working directory for the command. This must be one of the root directories of the project. - cd: String, -} - -pub struct TerminalTool { - project: Entity, - determine_shell: Shared>, -} - -impl TerminalTool { - pub fn new(project: Entity, 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() { - "bash".into() - } else { - get_system_shell() - } - }); - Self { - project, - determine_shell: determine_shell.shared(), - } - } -} - -impl AgentTool for TerminalTool { - type Input = TerminalToolInput; - type Output = String; - - fn name() -> &'static str { - "terminal" - } - - fn kind() -> acp::ToolKind { - acp::ToolKind::Execute - } - - fn initial_title(&self, input: Result) -> 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(), - 1 => MarkdownInlineCode(&format!( - "{} - {} more line", - first_line, remaining_line_count - )) - .to_string() - .into(), - n => MarkdownInlineCode(&format!("{} - {} more lines", first_line, n)) - .to_string() - .into(), - } - } else { - "Run terminal command".into() - } - } - - fn run( - self: Arc, - input: Self::Input, - event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task> { - 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}; {}) 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 (processed_content, finished_with_empty_output) = process_content( - &content, - &input.command, - exit_status.map(portable_pty::ExitStatus::from), - ); - - 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(); - - Ok(processed_content) - } - }) - } -} - -fn process_content( - content: &str, - command: &str, - exit_status: Option, -) -> (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(); - let is_empty = content.is_empty(); - let content = format!("```\n{content}\n```"); - let content = if should_truncate { - format!( - "Command output too long. The first {} bytes:\n\n{content}", - content.len(), - ) - } else { - content - }; - - let content = match exit_status { - Some(exit_status) if exit_status.success() => { - if is_empty { - "Command executed successfully.".to_string() - } else { - content - } - } - Some(exit_status) => { - if is_empty { - format!( - "Command \"{command}\" failed with exit code {}.", - exit_status.exit_code() - ) - } else { - format!( - "Command \"{command}\" failed with exit code {}.\n\n{content}", - exit_status.exit_code() - ) - } - } - None => { - format!( - "Command failed or was interrupted.\nPartial output captured:\n\n{}", - content, - ) - } - }; - (content, is_empty) -} - -fn working_dir( - input: &TerminalToolInput, - project: &Entity, - cx: &mut App, -) -> Result> { - let project = project.read(cx); - let cd = &input.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); - - match worktrees.next() { - Some(worktree) => { - anyhow::ensure!( - worktrees.next().is_none(), - "'.' is ambiguous in multi-root workspaces. Please specify a root directory explicitly.", - ); - Ok(Some(worktree.read(cx).abs_path().to_path_buf())) - } - None => Ok(None), - } - } else { - let input_path = Path::new(cd); - - if input_path.is_absolute() { - // Absolute paths are allowed, but only if they're in one of the project's worktrees. - if project - .worktrees(cx) - .any(|worktree| input_path.starts_with(&worktree.read(cx).abs_path())) - { - 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())); - } - - 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::ThreadEvent; - - 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::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::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(ThreadEvent::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; - } -} diff --git a/crates/agent2/src/tools/thinking_tool.rs b/crates/agent2/src/tools/thinking_tool.rs deleted file mode 100644 index 61fb9eb0d6..0000000000 --- a/crates/agent2/src/tools/thinking_tool.rs +++ /dev/null @@ -1,48 +0,0 @@ -use agent_client_protocol as acp; -use anyhow::Result; -use gpui::{App, SharedString, Task}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::sync::Arc; - -use crate::{AgentTool, ToolCallEventStream}; - -/// A tool for thinking through problems, brainstorming ideas, or planning without executing any actions. -/// 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: String, -} - -pub struct ThinkingTool; - -impl AgentTool for ThinkingTool { - type Input = ThinkingToolInput; - type Output = String; - - fn name() -> &'static str { - "thinking" - } - - fn kind() -> acp::ToolKind { - acp::ToolKind::Think - } - - fn initial_title(&self, _input: Result) -> SharedString { - "Thinking".into() - } - - fn run( - self: Arc, - input: Self::Input, - event_stream: ToolCallEventStream, - _cx: &mut App, - ) -> Task> { - event_stream.update_fields(acp::ToolCallUpdateFields { - content: Some(vec![input.content.into()]), - ..Default::default() - }); - Task::ready(Ok("Finished thinking.".to_string())) - } -} diff --git a/crates/agent2/src/tools/web_search_tool.rs b/crates/agent2/src/tools/web_search_tool.rs deleted file mode 100644 index d7a34bec29..0000000000 --- a/crates/agent2/src/tools/web_search_tool.rs +++ /dev/null @@ -1,127 +0,0 @@ -use std::sync::Arc; - -use crate::{AgentTool, ToolCallEventStream}; -use agent_client_protocol as acp; -use anyhow::{Result, anyhow}; -use cloud_llm_client::WebSearchResponse; -use gpui::{App, AppContext, Task}; -use language_model::{ - LanguageModelProviderId, LanguageModelToolResultContent, ZED_CLOUD_PROVIDER_ID, -}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -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. -/// Results will include snippets and links from relevant web pages. -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct WebSearchToolInput { - /// The search term or question to query on the web. - query: String, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(transparent)] -pub struct WebSearchToolOutput(WebSearchResponse); - -impl From for LanguageModelToolResultContent { - fn from(value: WebSearchToolOutput) -> Self { - serde_json::to_string(&value.0) - .expect("Failed to serialize WebSearchResponse") - .into() - } -} - -pub struct WebSearchTool; - -impl AgentTool for WebSearchTool { - type Input = WebSearchToolInput; - type Output = WebSearchToolOutput; - - fn name() -> &'static str { - "web_search" - } - - fn kind() -> acp::ToolKind { - acp::ToolKind::Fetch - } - - fn initial_title(&self, _input: Result) -> SharedString { - "Searching the Web".into() - } - - /// We currently only support Zed Cloud as a provider. - fn supported_provider(&self, provider: &LanguageModelProviderId) -> bool { - provider == &ZED_CLOUD_PROVIDER_ID - } - - fn run( - self: Arc, - input: Self::Input, - event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task> { - let Some(provider) = WebSearchRegistry::read_global(cx).active_provider() else { - return Task::ready(Err(anyhow!("Web search is not available."))); - }; - - let search_task = provider.search(input.query, cx); - cx.background_spawn(async move { - let response = match search_task.await { - Ok(response) => response, - Err(err) => { - event_stream.update_fields(acp::ToolCallUpdateFields { - title: Some("Web Search Failed".to_string()), - ..Default::default() - }); - return Err(err); - } - }; - - 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() - }); -} diff --git a/crates/agent_servers/Cargo.toml b/crates/agent_servers/Cargo.toml index 9f90f3a78a..dcffb05bc0 100644 --- a/crates/agent_servers/Cargo.toml +++ b/crates/agent_servers/Cargo.toml @@ -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", "dep:env_logger", "fs", "client/test-support", "dep:gpui_tokio", "reqwest_client/test-support"] +test-support = ["acp_thread/test-support", "gpui/test-support", "project/test-support"] e2e = [] [lints] @@ -17,43 +17,31 @@ 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 -agent_settings.workspace = true +agentic-coding-protocol.workspace = true anyhow.workspace = true -client = { workspace = true, optional = true } collections.workspace = true context_server.workspace = true -env_logger = { workspace = true, optional = true } -fs = { workspace = true, optional = 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 -reqwest_client = { workspace = true, optional = true } schemars.workspace = true -semver.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true smol.workspace = true strum.workspace = true tempfile.workspace = true -thiserror.workspace = true ui.workspace = true util.workspace = true uuid.workspace = true watch.workspace = true +indoc.workspace = true which.workspace = true workspace-hack.workspace = true @@ -62,12 +50,8 @@ 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"] } diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs deleted file mode 100644 index b4e897374a..0000000000 --- a/crates/agent_servers/src/acp.rs +++ /dev/null @@ -1,414 +0,0 @@ -use crate::AgentServerCommand; -use acp_thread::AgentConnection; -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::channel::oneshot; -use futures::io::BufReader; -use project::Project; -use serde::Deserialize; -use std::{any::Any, cell::RefCell}; -use std::{path::Path, rc::Rc}; -use thiserror::Error; - -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, - sessions: Rc>>, - auth_methods: Vec, - prompt_capabilities: acp::PromptCapabilities, - _io_task: Task>, -} - -pub struct AcpSession { - thread: WeakEntity, - suppress_abort_err: bool, -} - -pub async fn connect( - server_name: SharedString, - command: AgentServerCommand, - root_dir: &Path, - cx: &mut AsyncApp, -) -> Result> { - let conn = AcpConnection::stdio(server_name, command.clone(), root_dir, 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, - cx: &mut AsyncApp, - ) -> Result { - 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::piped()) - .kill_on_drop(true) - .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); - - 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(); - } - }) - .detach(); - - 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_load_error(LoadError::Exited { status }, cx) - }) - .ok(); - } - - anyhow::Ok(()) - } - }) - .detach(); - - 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, - }, - }, - }) - .await?; - - if response.protocol_version < MINIMUM_SUPPORTED_VERSION { - return Err(UnsupportedVersion.into()); - } - - Ok(Self { - auth_methods: response.auth_methods, - connection, - server_name, - sessions, - prompt_capabilities: response.agent_capabilities.prompt_capabilities, - _io_task: io_task, - }) - } -} - -impl AgentConnection for AcpConnection { - fn new_thread( - self: Rc, - project: Entity, - cwd: &Path, - cx: &mut App, - ) -> Task>> { - let conn = self.connection.clone(); - let sessions = self.sessions.clone(); - let cwd = cwd.to_path_buf(); - let context_server_store = project.read(cx).context_server_store().read(cx); - let mcp_servers = 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 { - 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(); - - 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 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.prompt_capabilities), - cx, - ) - })?; - - let session = AcpSession { - thread: thread.downgrade(), - suppress_abort_err: false, - }; - 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> { - 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, - params: acp::PromptRequest, - cx: &mut App, - ) -> Task> { - 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, - } - - 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 into_any(self: Rc) -> Rc { - self - } -} - -struct ClientDelegate { - sessions: Rc>>, - cx: AsyncApp, -} - -impl acp::Client for ClientDelegate { - async fn request_permission( - &self, - arguments: acp::RequestPermissionRequest, - ) -> Result { - 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 { - 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(()) - } -} diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index 7c7e124ca7..212bb74d8a 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -1,14 +1,14 @@ -mod acp; mod claude; -mod custom; +mod codex; mod gemini; +mod mcp_server; mod settings; -#[cfg(any(test, feature = "test-support"))] -pub mod e2e_tests; +#[cfg(test)] +mod e2e_tests; pub use claude::*; -pub use custom::*; +pub use codex::*; pub use gemini::*; pub use settings::*; @@ -20,7 +20,6 @@ use project::Project; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::{ - any::Any, path::{Path, PathBuf}, rc::Rc, sync::Arc, @@ -33,25 +32,17 @@ pub fn init(cx: &mut App) { pub trait AgentServer: Send { fn logo(&self) -> ui::IconName; - fn name(&self) -> SharedString; - fn empty_state_headline(&self) -> SharedString; - fn empty_state_message(&self) -> SharedString; - fn telemetry_id(&self) -> &'static str; + fn name(&self) -> &'static str; + fn empty_state_headline(&self) -> &'static str; + fn empty_state_message(&self) -> &'static str; fn connect( &self, + // these will go away when old_acp is fully removed root_dir: &Path, project: &Entity, cx: &mut App, ) -> Task>>; - - fn into_any(self: Rc) -> Rc; -} - -impl dyn AgentServer { - pub fn downcast(self: Rc) -> Option> { - self.into_any().downcast().ok() - } } impl std::fmt::Debug for AgentServerCommand { @@ -98,16 +89,15 @@ pub struct AgentServerCommand { } impl AgentServerCommand { - pub async fn resolve( + pub(crate) async fn resolve( path_bin_name: &'static str, extra_args: &[&'static str], - fallback_path: Option<&Path>, settings: Option, project: &Entity, cx: &mut AsyncApp, ) -> Option { if let Some(agent_settings) = settings { - Some(Self { + return Some(Self { path: agent_settings.command.path, args: agent_settings .command @@ -116,26 +106,15 @@ impl AgentServerCommand { .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 { + find_bin_in_path(path_bin_name, project, cx) + .await + .map(|path| 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 - } - }), - } + }) } } } diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 250e564526..6565786204 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -1,27 +1,19 @@ -mod edit_tool; mod mcp_server; -mod permission_tool; -mod read_tool; pub mod tools; -mod write_tool; -use action_log::ActionLog; use collections::HashMap; use context_server::listener::McpServerTool; -use language_models::provider::anthropic::AnthropicLanguageModelProvider; use project::Project; use settings::SettingsStore; use smol::process::Child; -use std::any::Any; use std::cell::RefCell; use std::fmt::Display; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::rc::Rc; -use util::command::new_smol_command; use uuid::Uuid; use agent_client_protocol as acp; -use anyhow::{Context as _, Result, anyhow}; +use anyhow::{Result, anyhow}; use futures::channel::oneshot; use futures::{AsyncBufReadExt, AsyncWriteExt}; use futures::{ @@ -30,33 +22,29 @@ use futures::{ io::BufReader, select_biased, }; -use gpui::{App, AppContext, AsyncApp, Entity, SharedString, Task, WeakEntity}; +use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity}; use serde::{Deserialize, Serialize}; -use util::{ResultExt, debug_panic}; +use util::ResultExt; use crate::claude::mcp_server::{ClaudeZedMcpServer, McpConfig}; use crate::claude::tools::ClaudeTool; use crate::{AgentServer, AgentServerCommand, AllAgentServersSettings}; -use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError, MentionUri}; +use acp_thread::{AcpThread, AgentConnection}; #[derive(Clone)] pub struct ClaudeCode; impl AgentServer for ClaudeCode { - fn telemetry_id(&self) -> &'static str { - "claude-code" + fn name(&self) -> &'static str { + "Claude Code" } - fn name(&self) -> SharedString { - "Claude Code".into() - } - - fn empty_state_headline(&self) -> SharedString { + fn empty_state_headline(&self) -> &'static str { self.name() } - fn empty_state_message(&self) -> SharedString { - "How can I help you today?".into() + fn empty_state_message(&self) -> &'static str { + "How can I help you today?" } fn logo(&self) -> ui::IconName { @@ -75,10 +63,6 @@ impl AgentServer for ClaudeCode { Task::ready(Ok(Rc::new(connection) as _)) } - - fn into_any(self: Rc) -> Rc { - self - } } struct ClaudeAgentConnection { @@ -86,51 +70,20 @@ struct ClaudeAgentConnection { } impl AgentConnection for ClaudeAgentConnection { + fn name(&self) -> &'static str { + ClaudeCode.name() + } + fn new_thread( self: Rc, project: Entity, cwd: &Path, - cx: &mut App, + cx: &mut AsyncApp, ) -> Task>> { let cwd = cwd.to_owned(); cx.spawn(async move |cx| { - let settings = cx.read_global(|settings: &SettingsStore, _| { - settings.get::(None).claude.clone() - })?; - - let Some(command) = AgentServerCommand::resolve( - "claude", - &[], - Some(&util::paths::home_dir().join(".claude/local/claude")), - settings, - &project, - cx, - ) - .await - else { - return Err(LoadError::NotInstalled { - error_message: "Failed to find Claude Code binary".into(), - install_message: "Install Claude Code".into(), - install_command: "npm install -g @anthropic-ai/claude-code@latest".into(), - }.into()); - }; - - let api_key = - cx.update(AnthropicLanguageModelProvider::api_key)? - .await - .map_err(|err| { - if err.is::() { - anyhow!(AuthRequired::new().with_language_model_provider( - language_model::ANTHROPIC_PROVIDER_ID - )) - } else { - anyhow!(err) - } - })?; - let (mut thread_tx, thread_rx) = watch::channel(WeakEntity::new_invalid()); - let fs = project.read_with(cx, |project, _cx| project.fs().clone())?; - let permission_mcp_server = ClaudeZedMcpServer::new(thread_rx.clone(), fs, cx).await?; + let permission_mcp_server = ClaudeZedMcpServer::new(thread_rx.clone(), cx).await?; let mut mcp_servers = HashMap::default(); mcp_servers.insert( @@ -148,6 +101,16 @@ impl AgentConnection for ClaudeAgentConnection { .await?; mcp_config_file.flush().await?; + let settings = cx.read_global(|settings: &SettingsStore, _| { + settings.get::(None).claude.clone() + })?; + + let Some(command) = + AgentServerCommand::resolve("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(); @@ -155,125 +118,64 @@ impl AgentConnection for ClaudeAgentConnection { log::trace!("Starting session with id: {}", session_id); - let mut child = spawn_claude( - &command, - ClaudeSessionMode::Start, - session_id.clone(), - api_key, - &mcp_config_path, - &cwd, - )?; + cx.background_spawn({ + let session_id = session_id.clone(); + async move { + let mut outgoing_rx = Some(outgoing_rx); - 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")?; + let mut child = spawn_claude( + &command, + ClaudeSessionMode::Start, + session_id.clone(), + &mcp_config_path, + &cwd, + ) + .await?; - let pid = child.id(); - log::trace!("Spawned (pid: {})", pid); + let pid = child.id(); + log::trace!("Spawned (pid: {})", pid); - 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(); + ClaudeAgentSession::handle_io( + outgoing_rx.take().unwrap(), + incoming_message_tx.clone(), + child.stdin.take().unwrap(), + child.stdout.take().unwrap(), + ) + .await?; + + log::trace!("Stopped (pid: {})", pid); + + drop(mcp_config_path); + anyhow::Ok(()) } }) .detach(); - 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 end_turn_tx = Rc::new(RefCell::new(None)); let handler_task = cx.spawn({ - let turn_state = turn_state.clone(); - let mut thread_rx = thread_rx.clone(); + let end_turn_tx = end_turn_tx.clone(); + let 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(), + end_turn_tx.clone(), cx, ) .await } - - if let Some(status) = child.status().await.log_err() - && let Some(thread) = thread_rx.recv().await.ok() - { - let version = claude_version(command.path.clone(), cx).await.log_err(); - let help = claude_help(command.path.clone(), cx).await.log_err(); - thread - .update(cx, |thread, cx| { - let error = if let Some(version) = version - && let Some(help) = help - && (!help.contains("--input-format") - || !help.contains("--session-id")) - { - LoadError::Unsupported { - error_message: format!( - "Your installed version of Claude Code ({}, version {}) does not have required features for use with Zed.", - command.path.to_string_lossy(), - version, - ) - .into(), - upgrade_message: "Upgrade Claude Code to latest".into(), - upgrade_command: format!( - "{} update", - command.path.to_string_lossy() - ), - } - } else { - LoadError::Exited { status } - }; - thread.emit_load_error(error, cx); - }) - .ok(); - } } }); - let action_log = cx.new(|_| ActionLog::new(project.clone()))?; - let thread = cx.new(|cx| { - AcpThread::new( - "Claude Code", - self.clone(), - project, - action_log, - session_id.clone(), - watch::Receiver::constant(acp::PromptCapabilities { - image: true, - audio: false, - embedded_context: true, - }), - cx, - ) - })?; + let thread = + cx.new(|cx| AcpThread::new(self.clone(), project, session_id.clone(), cx))?; thread_tx.send(thread.downgrade())?; let session = ClaudeAgentSession { outgoing_tx, - turn_state, + end_turn_tx, _handler_task: handler_task, _mcp_server: Some(permission_mcp_server), }; @@ -284,20 +186,11 @@ impl AgentConnection for ClaudeAgentConnection { }) } - fn auth_methods(&self) -> &[acp::AuthMethod] { - &[] - } - - fn authenticate(&self, _: acp::AuthMethodId, _cx: &mut App) -> Task> { + fn authenticate(&self, _cx: &mut App) -> Task> { Task::ready(Err(anyhow!("Authentication not supported"))) } - fn prompt( - &self, - _id: Option, - params: acp::PromptRequest, - cx: &mut App, - ) -> Task> { + fn prompt(&self, params: acp::PromptArguments, cx: &mut App) -> Task> { let sessions = self.sessions.borrow(); let Some(session) = sessions.get(¶ms.session_id) else { return Task::ready(Err(anyhow!( @@ -306,15 +199,30 @@ impl AgentConnection for ClaudeAgentConnection { ))); }; - let (end_tx, end_rx) = oneshot::channel(); - session.turn_state.replace(TurnState::InProgress { end_tx }); + let (tx, rx) = oneshot::channel(); + session.end_turn_tx.borrow_mut().replace(tx); - let content = acp_content_to_claude(params.prompt); + 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::Chunks(content), + content: Content::UntaggedText(content), id: None, model: None, stop_reason: None, @@ -326,42 +234,24 @@ impl AgentConnection for ClaudeAgentConnection { return Task::ready(Err(anyhow!(err))); } - cx.foreground_executor().spawn(async move { end_rx.await? }) + cx.foreground_executor().spawn(async move { + rx.await??; + Ok(()) + }) } fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) { let sessions = self.sessions.borrow(); - let Some(session) = sessions.get(session_id) else { + 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 canceled 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, - }) + .unbounded_send(SdkMessage::new_interrupt_message()) .log_err(); } - - fn into_any(self: Rc) -> Rc { - self - } } #[derive(Clone, Copy)] @@ -371,11 +261,10 @@ enum ClaudeSessionMode { Resume, } -fn spawn_claude( +async fn spawn_claude( command: &AgentServerCommand, mode: ClaudeSessionMode, session_id: acp::SessionId, - api_key: language_models::provider::anthropic::ApiKey, mcp_config_path: &Path, root_dir: &Path, ) -> Result { @@ -393,204 +282,56 @@ fn spawn_claude( &format!( "mcp__{}__{}", mcp_server::SERVER_NAME, - permission_tool::PermissionTool::NAME, + mcp_server::PermissionTool::NAME, ), "--allowedTools", &format!( - "mcp__{}__{}", + "mcp__{}__{},mcp__{}__{}", mcp_server::SERVER_NAME, - read_tool::ReadTool::NAME + mcp_server::EditTool::NAME, + mcp_server::SERVER_NAME, + mcp_server::ReadTool::NAME ), "--disallowedTools", - "Read,Write,Edit,MultiEdit", + "Read,Edit", ]) .args(match mode { ClaudeSessionMode::Start => ["--session-id".to_string(), session_id.to_string()], ClaudeSessionMode::Resume => ["--resume".to_string(), session_id.to_string()], }) .args(command.args.iter().map(|arg| arg.as_str())) - .envs(command.env.iter().flatten()) - .env("ANTHROPIC_API_KEY", api_key.key) .current_dir(root_dir) .stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::piped()) + .stderr(std::process::Stdio::inherit()) .kill_on_drop(true) .spawn()?; Ok(child) } -fn claude_version(path: PathBuf, cx: &mut AsyncApp) -> Task> { - cx.background_spawn(async move { - let output = new_smol_command(path).arg("--version").output().await?; - let output = String::from_utf8(output.stdout)?; - let version = output - .trim() - .strip_suffix(" (Claude Code)") - .context("parsing Claude version")?; - let version = semver::Version::parse(version)?; - anyhow::Ok(version) - }) -} - -fn claude_help(path: PathBuf, cx: &mut AsyncApp) -> Task> { - cx.background_spawn(async move { - let output = new_smol_command(path).arg("--help").output().await?; - let output = String::from_utf8(output.stdout)?; - anyhow::Ok(output) - }) -} - struct ClaudeAgentSession { outgoing_tx: UnboundedSender, - turn_state: Rc>, + end_turn_tx: Rc>>>>, _mcp_server: Option, _handler_task: Task<()>, } -#[derive(Debug, Default)] -enum TurnState { - #[default] - None, - InProgress { - end_tx: oneshot::Sender>, - }, - CancelRequested { - end_tx: oneshot::Sender>, - request_id: String, - }, - CancelConfirmed { - end_tx: oneshot::Sender>, - }, -} - -impl TurnState { - fn is_canceled(&self) -> bool { - matches!(self, TurnState::CancelConfirmed { .. }) - } - - fn end_tx(self) -> Option>> { - 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>, message: SdkMessage, - turn_state: Rc>, + end_turn_tx: Rc>>>>, 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 { + 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) => { - if !turn_state.borrow().is_canceled() { - 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| { - let id = acp::ToolCallId(tool_use_id.into()); - let set_new_content = !content.is_empty() - && thread.tool_call(&id).is_none_or(|(_, tool_call)| { - // preserve rich diff if we have one - tool_call.diffs().next().is_none() - }); - - thread.update_tool_call( - acp::ToolCallUpdate { - id, - fields: acp::ToolCallUpdateFields { - status: if turn_state.borrow().is_canceled() { - // Do not set to completed if turn was canceled - None - } else { - Some(acp::ToolCallStatus::Completed) - }, - content: set_new_content - .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 { source } => { - if !turn_state.borrow().is_canceled() { - thread - .update(cx, |thread, cx| { - thread.push_user_content_block(None, source.into(), cx) - }) - .log_err(); - } - } - - 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 { + | SdkMessage::User { message, session_id: _, } => { @@ -613,24 +354,6 @@ impl ClaudeAgentSession { }) .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); @@ -651,25 +374,38 @@ impl ClaudeAgentSession { thread.upsert_tool_call( claude_tool.as_acp(acp::ToolCallId(id.into())), cx, - )?; + ); } - anyhow::Ok(()) }) .log_err(); } - ContentChunk::ToolResult { .. } | ContentChunk::WebSearchToolResult => { - debug_panic!( - "Should not get tool results with role: assistant. should we handle this?" - ); - } - ContentChunk::Image { source } => { + ContentChunk::ToolResult { + content, + tool_use_id, + } => { + let content = content.to_string(); thread .update(cx, |thread, cx| { - thread.push_assistant_content_block(source.into(), false, cx) + thread.update_tool_call( + acp::ToolCallUpdate { + id: acp::ToolCallId(tool_use_id.into()), + fields: acp::ToolCallUpdateFields { + status: Some(acp::ToolCallStatus::Completed), + content: (!content.is_empty()) + .then(|| vec![content.into()]), + ..Default::default() + }, + }, + cx, + ) }) .log_err(); } - ContentChunk::Document => { + ContentChunk::Image + | ContentChunk::Document + | ContentChunk::Thinking + | ContentChunk::RedactedThinking + | ContentChunk::WebSearchToolResult => { thread .update(cx, |thread, cx| { thread.push_assistant_content_block( @@ -689,40 +425,20 @@ impl ClaudeAgentSession { result, .. } => { - let turn_state = turn_state.take(); - let was_canceled = turn_state.is_canceled(); - 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_canceled && 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(); + if let Some(end_turn_tx) = end_turn_tx.borrow_mut().take() { + if is_error { + end_turn_tx + .send(Err(anyhow!( + "Error: {}", + result.unwrap_or_else(|| subtype.to_string()) + ))) + .ok(); + } else { + end_turn_tx.send(Ok(())).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 { .. } => {} + SdkMessage::System { .. } | SdkMessage::ControlResponse { .. } => {} } } @@ -797,7 +513,7 @@ impl Content { pub fn chunks(self) -> impl Iterator { match self { Self::Chunks(chunks) => chunks.into_iter(), - Self::UntaggedText(text) => vec![ContentChunk::Text { text }].into_iter(), + Self::UntaggedText(text) => vec![ContentChunk::Text { text: text.clone() }].into_iter(), } } } @@ -831,58 +547,26 @@ enum ContentChunk { content: Content, tool_use_id: String, }, - Thinking { - thinking: String, - }, - RedactedThinking, - Image { - source: ImageSource, - }, // TODO + Image, Document, + Thinking, + RedactedThinking, WebSearchToolResult, #[serde(untagged)] UntaggedText(String), } -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "type", rename_all = "snake_case")] -enum ImageSource { - Base64 { data: String, media_type: String }, - Url { url: String }, -} - -impl Into for ImageSource { - fn into(self) -> acp::ContentBlock { - match self { - ImageSource::Base64 { data, media_type } => { - acp::ContentBlock::Image(acp::ImageContent { - annotations: None, - data, - mime_type: media_type, - uri: None, - }) - } - ImageSource::Url { url } => acp::ContentBlock::Image(acp::ImageContent { - annotations: None, - data: "".to_string(), - mime_type: "".to_string(), - uri: Some(url), - }), - } - } -} - 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::Image | ContentChunk::Document + | ContentChunk::Thinking + | ContentChunk::RedactedThinking | ContentChunk::ToolUse { .. } | ContentChunk::WebSearchToolResult => { write!(f, "\n{:?}\n", &self) @@ -975,7 +659,7 @@ struct ControlResponse { subtype: ResultErrorType, } -#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] enum ResultErrorType { Success, @@ -993,84 +677,22 @@ impl Display for ResultErrorType { } } -fn acp_content_to_claude(prompt: Vec) -> Vec { - let mut content = Vec::with_capacity(prompt.len()); - let mut context = Vec::with_capacity(prompt.len()); +impl SdkMessage { + fn new_interrupt_message() -> Self { + use rand::Rng; + // In the Claude Code TS SDK they just generate a random 12 character string, + // `Math.random().toString(36).substring(2, 15)` + let request_id = rand::thread_rng() + .sample_iter(&rand::distributions::Alphanumeric) + .take(12) + .map(char::from) + .collect(); - for chunk in prompt { - match chunk { - acp::ContentBlock::Text(text_content) => { - content.push(ContentChunk::Text { - text: text_content.text, - }); - } - acp::ContentBlock::ResourceLink(resource_link) => { - match MentionUri::parse(&resource_link.uri) { - Ok(uri) => { - content.push(ContentChunk::Text { - text: format!("{}", uri.as_link()), - }); - } - Err(_) => { - content.push(ContentChunk::Text { - text: resource_link.uri, - }); - } - } - } - acp::ContentBlock::Resource(resource) => match resource.resource { - acp::EmbeddedResourceResource::TextResourceContents(resource) => { - match MentionUri::parse(&resource.uri) { - Ok(uri) => { - content.push(ContentChunk::Text { - text: format!("{}", uri.as_link()), - }); - } - Err(_) => { - content.push(ContentChunk::Text { - text: resource.uri.clone(), - }); - } - } - - context.push(ContentChunk::Text { - text: format!( - "\n\n{}\n", - resource.uri, resource.text - ), - }); - } - acp::EmbeddedResourceResource::BlobResourceContents(_) => { - // Unsupported by SDK - } - }, - acp::ContentBlock::Image(acp::ImageContent { - data, mime_type, .. - }) => content.push(ContentChunk::Image { - source: ImageSource::Base64 { - data, - media_type: mime_type, - }, - }), - acp::ContentBlock::Audio(_) => { - // Unsupported by SDK - } + Self::ControlRequest { + request_id, + request: ControlRequest::Interrupt, } } - - content.extend(context); - content -} - -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)] @@ -1091,11 +713,9 @@ enum PermissionMode { #[cfg(test)] pub(crate) mod tests { use super::*; - use crate::e2e_tests; - use gpui::TestAppContext; use serde_json::json; - crate::common_e2e_tests!(async |_, _, _| ClaudeCode, allow_option_id = "allow"); + crate::common_e2e_tests!(ClaudeCode, allow_option_id = "allow"); pub fn local_command() -> AgentServerCommand { AgentServerCommand { @@ -1105,68 +725,6 @@ pub(crate) mod tests { } } - #[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.is_empty(), "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!"); @@ -1278,100 +836,4 @@ pub(crate) mod tests { _ => panic!("Expected ToolResult variant"), } } - - #[test] - fn test_acp_content_to_claude() { - let acp_content = vec![ - acp::ContentBlock::Text(acp::TextContent { - text: "Hello world".to_string(), - annotations: None, - }), - acp::ContentBlock::Image(acp::ImageContent { - data: "base64data".to_string(), - mime_type: "image/png".to_string(), - annotations: None, - uri: None, - }), - acp::ContentBlock::ResourceLink(acp::ResourceLink { - uri: "file:///path/to/example.rs".to_string(), - name: "example.rs".to_string(), - annotations: None, - description: None, - mime_type: None, - size: None, - title: None, - }), - acp::ContentBlock::Resource(acp::EmbeddedResource { - annotations: None, - resource: acp::EmbeddedResourceResource::TextResourceContents( - acp::TextResourceContents { - mime_type: None, - text: "fn main() { println!(\"Hello!\"); }".to_string(), - uri: "file:///path/to/code.rs".to_string(), - }, - ), - }), - acp::ContentBlock::ResourceLink(acp::ResourceLink { - uri: "invalid_uri_format".to_string(), - name: "invalid.txt".to_string(), - annotations: None, - description: None, - mime_type: None, - size: None, - title: None, - }), - ]; - - let claude_content = acp_content_to_claude(acp_content); - - assert_eq!(claude_content.len(), 6); - - match &claude_content[0] { - ContentChunk::Text { text } => assert_eq!(text, "Hello world"), - _ => panic!("Expected Text chunk"), - } - - match &claude_content[1] { - ContentChunk::Image { source } => match source { - ImageSource::Base64 { data, media_type } => { - assert_eq!(data, "base64data"); - assert_eq!(media_type, "image/png"); - } - _ => panic!("Expected Base64 image source"), - }, - _ => panic!("Expected Image chunk"), - } - - match &claude_content[2] { - ContentChunk::Text { text } => { - assert!(text.contains("example.rs")); - assert!(text.contains("file:///path/to/example.rs")); - } - _ => panic!("Expected Text chunk for ResourceLink"), - } - - match &claude_content[3] { - ContentChunk::Text { text } => { - assert!(text.contains("code.rs")); - assert!(text.contains("file:///path/to/code.rs")); - } - _ => panic!("Expected Text chunk for Resource"), - } - - match &claude_content[4] { - ContentChunk::Text { text } => { - assert_eq!(text, "invalid_uri_format"); - } - _ => panic!("Expected Text chunk for invalid URI"), - } - - match &claude_content[5] { - ContentChunk::Text { text } => { - assert!(text.contains("")); - assert!(text.contains("fn main() { println!(\"Hello!\"); }")); - assert!(text.contains("")); - } - _ => panic!("Expected Text chunk for context"), - } - } } diff --git a/crates/agent_servers/src/claude/edit_tool.rs b/crates/agent_servers/src/claude/edit_tool.rs deleted file mode 100644 index a8d93c3f3d..0000000000 --- a/crates/agent_servers/src/claude/edit_tool.rs +++ /dev/null @@ -1,178 +0,0 @@ -use acp_thread::AcpThread; -use anyhow::Result; -use context_server::{ - listener::{McpServerTool, ToolResponse}, - types::{ToolAnnotations, ToolResponseContent}, -}; -use gpui::{AsyncApp, WeakEntity}; -use language::unified_diff; -use util::markdown::MarkdownCodeBlock; - -use crate::tools::EditToolParams; - -#[derive(Clone)] -pub struct EditTool { - thread_rx: watch::Receiver>, -} - -impl EditTool { - pub fn new(thread_rx: watch::Receiver>) -> Self { - Self { thread_rx } - } -} - -impl McpServerTool for EditTool { - type Input = EditToolParams; - type Output = (); - - const NAME: &'static str = "Edit"; - - 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> { - 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, diff) = cx - .background_executor() - .spawn(async move { - let new_content = content.replace(&input.old_text, &input.new_text); - if new_content == content { - return Err(anyhow::anyhow!("Failed to find `old_text`",)); - } - let diff = unified_diff(&content, &new_content); - - Ok((new_content, diff)) - }) - .await?; - - thread - .update(cx, |thread, cx| { - thread.write_text_file(input.abs_path, new_content, cx) - })? - .await?; - - Ok(ToolResponse { - content: vec![ToolResponseContent::Text { - text: MarkdownCodeBlock { - tag: "diff", - text: diff.as_str().trim_end_matches('\n'), - } - .to_string(), - }], - structured_content: (), - }) - } -} - -#[cfg(test)] -mod tests { - use std::rc::Rc; - - use acp_thread::{AgentConnection, StubAgentConnection}; - use gpui::{Entity, TestAppContext}; - use indoc::indoc; - use project::{FakeFs, Project}; - use serde_json::json; - use settings::SettingsStore; - use util::path; - - use super::*; - - #[gpui::test] - async fn old_text_not_found(cx: &mut TestAppContext) { - let (_thread, tool) = init_test(cx).await; - - let result = tool - .run( - EditToolParams { - abs_path: path!("/root/file.txt").into(), - old_text: "hi".into(), - new_text: "bye".into(), - }, - &mut cx.to_async(), - ) - .await; - - assert_eq!(result.unwrap_err().to_string(), "Failed to find `old_text`"); - } - - #[gpui::test] - async fn found_and_replaced(cx: &mut TestAppContext) { - let (_thread, tool) = init_test(cx).await; - - let result = tool - .run( - EditToolParams { - abs_path: path!("/root/file.txt").into(), - old_text: "hello".into(), - new_text: "hi".into(), - }, - &mut cx.to_async(), - ) - .await; - - assert_eq!( - result.unwrap().content[0].text().unwrap(), - indoc! { - r" - ```diff - @@ -1,1 +1,1 @@ - -hello - +hi - ``` - " - } - ); - } - - async fn init_test(cx: &mut TestAppContext) -> (Entity, EditTool) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - language::init(cx); - Project::init_settings(cx); - }); - - let connection = Rc::new(StubAgentConnection::new()); - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root"), - json!({ - "file.txt": "hello" - }), - ) - .await; - let project = Project::test(fs, [path!("/root").as_ref()], cx).await; - let (mut thread_tx, thread_rx) = watch::channel(WeakEntity::new_invalid()); - - let thread = cx - .update(|cx| connection.new_thread(project, path!("/test").as_ref(), cx)) - .await - .unwrap(); - - thread_tx.send(thread.downgrade()).unwrap(); - - (thread, EditTool::new(thread_rx)) - } -} diff --git a/crates/agent_servers/src/claude/mcp_server.rs b/crates/agent_servers/src/claude/mcp_server.rs index 6442c784b5..cc303016f1 100644 --- a/crates/agent_servers/src/claude/mcp_server.rs +++ b/crates/agent_servers/src/claude/mcp_server.rs @@ -1,22 +1,18 @@ use std::path::PathBuf; -use std::sync::Arc; -use crate::claude::edit_tool::EditTool; -use crate::claude::permission_tool::PermissionTool; -use crate::claude::read_tool::ReadTool; -use crate::claude::write_tool::WriteTool; +use crate::claude::tools::{ClaudeTool, EditToolParams, ReadToolParams}; use acp_thread::AcpThread; -#[cfg(not(test))] -use anyhow::Context as _; -use anyhow::Result; +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, - ToolsCapabilities, requests, + ToolAnnotations, ToolResponseContent, ToolsCapabilities, requests, }; use gpui::{App, AsyncApp, Task, WeakEntity}; -use project::Fs; -use serde::Serialize; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; pub struct ClaudeZedMcpServer { server: context_server::listener::McpServer, @@ -27,16 +23,20 @@ pub const SERVER_NAME: &str = "zed"; impl ClaudeZedMcpServer { pub async fn new( thread_rx: watch::Receiver>, - fs: Arc, cx: &AsyncApp, ) -> Result { let mut mcp_server = context_server::listener::McpServer::new(cx).await?; mcp_server.handle_request::(Self::handle_initialize); - mcp_server.add_tool(PermissionTool::new(fs.clone(), thread_rx.clone())); - mcp_server.add_tool(ReadTool::new(thread_rx.clone())); - mcp_server.add_tool(EditTool::new(thread_rx.clone())); - mcp_server.add_tool(WriteTool::new(thread_rx.clone())); + 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 }) } @@ -97,3 +97,206 @@ pub struct McpServerConfig { #[serde(skip_serializing_if = "Option::is_none")] pub env: Option>, } + +// Tools + +#[derive(Clone)] +pub struct PermissionTool { + thread_rx: watch::Receiver>, +} + +#[derive(Deserialize, JsonSchema, Debug)] +pub struct PermissionToolParams { + tool_name: String, + input: serde_json::Value, + tool_use_id: Option, +} + +#[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> { + 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_permission( + claude_tool.as_acp(tool_call_id), + vec![ + acp::PermissionOption { + id: allow_option_id.clone(), + label: "Allow".into(), + kind: acp::PermissionOptionKind::AllowOnce, + }, + acp::PermissionOption { + id: reject_option_id.clone(), + label: "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>, +} + +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> { + 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>, +} + +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> { + 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: (), + }) + } +} diff --git a/crates/agent_servers/src/claude/permission_tool.rs b/crates/agent_servers/src/claude/permission_tool.rs deleted file mode 100644 index 96a24105e8..0000000000 --- a/crates/agent_servers/src/claude/permission_tool.rs +++ /dev/null @@ -1,158 +0,0 @@ -use std::sync::Arc; - -use acp_thread::AcpThread; -use agent_client_protocol as acp; -use agent_settings::AgentSettings; -use anyhow::{Context as _, Result}; -use context_server::{ - listener::{McpServerTool, ToolResponse}, - types::ToolResponseContent, -}; -use gpui::{AsyncApp, WeakEntity}; -use project::Fs; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{Settings as _, update_settings_file}; -use util::debug_panic; - -use crate::tools::ClaudeTool; - -#[derive(Clone)] -pub struct PermissionTool { - fs: Arc, - thread_rx: watch::Receiver>, -} - -/// Request permission for tool calls -#[derive(Deserialize, JsonSchema, Debug)] -pub struct PermissionToolParams { - tool_name: String, - input: serde_json::Value, - tool_use_id: Option, -} - -#[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 PermissionTool { - pub fn new(fs: Arc, thread_rx: watch::Receiver>) -> Self { - Self { fs, thread_rx } - } -} - -impl McpServerTool for PermissionTool { - type Input = PermissionToolParams; - type Output = (); - - const NAME: &'static str = "Confirmation"; - - async fn run( - &self, - input: Self::Input, - cx: &mut AsyncApp, - ) -> Result> { - if agent_settings::AgentSettings::try_read_global(cx, |settings| { - settings.always_allow_tool_actions - }) - .unwrap_or(false) - { - let response = PermissionToolResponse { - behavior: PermissionToolBehavior::Allow, - updated_input: input.input, - }; - - return Ok(ToolResponse { - content: vec![ToolResponseContent::Text { - text: serde_json::to_string(&response)?, - }], - structured_content: (), - }); - } - - 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()); - - const ALWAYS_ALLOW: &str = "always_allow"; - const ALLOW: &str = "allow"; - const REJECT: &str = "reject"; - - let chosen_option = thread - .update(cx, |thread, cx| { - thread.request_tool_call_authorization( - claude_tool.as_acp(tool_call_id).into(), - vec![ - acp::PermissionOption { - id: acp::PermissionOptionId(ALWAYS_ALLOW.into()), - name: "Always Allow".into(), - kind: acp::PermissionOptionKind::AllowAlways, - }, - acp::PermissionOption { - id: acp::PermissionOptionId(ALLOW.into()), - name: "Allow".into(), - kind: acp::PermissionOptionKind::AllowOnce, - }, - acp::PermissionOption { - id: acp::PermissionOptionId(REJECT.into()), - name: "Reject".into(), - kind: acp::PermissionOptionKind::RejectOnce, - }, - ], - cx, - ) - })?? - .await?; - - let response = match chosen_option.0.as_ref() { - ALWAYS_ALLOW => { - cx.update(|cx| { - update_settings_file::(self.fs.clone(), cx, |settings, _| { - settings.set_always_allow_tool_actions(true); - }); - })?; - - PermissionToolResponse { - behavior: PermissionToolBehavior::Allow, - updated_input: input.input, - } - } - ALLOW => PermissionToolResponse { - behavior: PermissionToolBehavior::Allow, - updated_input: input.input, - }, - REJECT => PermissionToolResponse { - behavior: PermissionToolBehavior::Deny, - updated_input: input.input, - }, - opt => { - debug_panic!("Unexpected option: {}", opt); - PermissionToolResponse { - behavior: PermissionToolBehavior::Deny, - updated_input: input.input, - } - } - }; - - Ok(ToolResponse { - content: vec![ToolResponseContent::Text { - text: serde_json::to_string(&response)?, - }], - structured_content: (), - }) - } -} diff --git a/crates/agent_servers/src/claude/read_tool.rs b/crates/agent_servers/src/claude/read_tool.rs deleted file mode 100644 index cbe25876b3..0000000000 --- a/crates/agent_servers/src/claude/read_tool.rs +++ /dev/null @@ -1,59 +0,0 @@ -use acp_thread::AcpThread; -use anyhow::Result; -use context_server::{ - listener::{McpServerTool, ToolResponse}, - types::{ToolAnnotations, ToolResponseContent}, -}; -use gpui::{AsyncApp, WeakEntity}; - -use crate::tools::ReadToolParams; - -#[derive(Clone)] -pub struct ReadTool { - thread_rx: watch::Receiver>, -} - -impl ReadTool { - pub fn new(thread_rx: watch::Receiver>) -> Self { - Self { thread_rx } - } -} - -impl McpServerTool for ReadTool { - type Input = ReadToolParams; - type Output = (); - - const NAME: &'static str = "Read"; - - 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> { - 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: (), - }) - } -} diff --git a/crates/agent_servers/src/claude/tools.rs b/crates/agent_servers/src/claude/tools.rs index 3231903001..6acb6355aa 100644 --- a/crates/agent_servers/src/claude/tools.rs +++ b/crates/agent_servers/src/claude/tools.rs @@ -34,7 +34,6 @@ impl ClaudeTool { // 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()), - "mcp__zed__Write" => Self::Write(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()), @@ -58,7 +57,7 @@ impl ClaudeTool { Self::Terminal(None) } else { Self::Other { - name: tool_name, + name: tool_name.to_string(), input, } } @@ -94,7 +93,7 @@ impl ClaudeTool { } Self::MultiEdit(None) => "Multi Edit".into(), Self::Write(Some(params)) => { - format!("Write {}", params.abs_path.display()) + format!("Write {}", params.file_path.display()) } Self::Write(None) => "Write".into(), Self::Glob(Some(params)) => { @@ -144,6 +143,25 @@ impl ClaudeTool { 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::TodoWrite(Some(params)) => vec![ + params + .todos + .iter() + .map(|todo| { + format!( + "- {} {}: {}", + match todo.status { + TodoStatus::Completed => "✅", + TodoStatus::InProgress => "🚧", + TodoStatus::Pending => "⬜", + }, + todo.priority, + todo.content + ) + }) + .join("\n") + .into(), + ], Self::ExitPlanMode(Some(params)) => vec![params.plan.clone().into()], Self::Edit(Some(params)) => vec![acp::ToolCallContent::Diff { diff: acp::Diff { @@ -154,7 +172,7 @@ impl ClaudeTool { }], Self::Write(Some(params)) => vec![acp::ToolCallContent::Diff { diff: acp::Diff { - path: params.abs_path.clone(), + path: params.file_path.clone(), old_text: None, new_text: params.content.clone(), }, @@ -175,10 +193,6 @@ impl ClaudeTool { }) .unwrap_or_default() } - Self::TodoWrite(Some(_)) => { - // These are mapped to plan updates later - vec![] - } Self::Task(None) | Self::NotebookRead(None) | Self::NotebookEdit(None) @@ -230,10 +244,7 @@ impl ClaudeTool { line: None, }] } - Self::Write(Some(WriteToolParams { - abs_path: file_path, - .. - })) => { + Self::Write(Some(WriteToolParams { file_path, .. })) => { vec![acp::ToolCallLocation { path: file_path.clone(), line: None, @@ -297,29 +308,14 @@ impl ClaudeTool { id, kind: self.kind(), status: acp::ToolCallStatus::InProgress, - title: self.label(), + label: self.label(), content: self.content(), locations: self.locations(), raw_input: None, - raw_output: None, } } } -/// Edit a file. -/// -/// In sessions with mcp__zed__Edit always use it instead of Edit as it will -/// allow the user to conveniently review changes. -/// -/// File editing instructions: -/// - The `old_text` param must match existing file content, including indentation. -/// - The `old_text` param must come from the actual file, not an outline. -/// - The `old_text` section must not be empty. -/// - Be minimal with replacements: -/// - For unique lines, include only those lines. -/// - For non-unique lines, include enough context to identify them. -/// - Do not escape quotes, newlines, or other characters. -/// - Only edit the specified file. #[derive(Deserialize, JsonSchema, Debug)] pub struct EditToolParams { /// The absolute path to the file to read. @@ -330,11 +326,6 @@ pub struct EditToolParams { pub new_text: String, } -/// Reads the content of the given file in the project. -/// -/// Never attempt to read a path that hasn't been previously mentioned. -/// -/// In sessions with mcp__zed__Read always use it instead of Read as it contains the most up-to-date contents. #[derive(Deserialize, JsonSchema, Debug)] pub struct ReadToolParams { /// The absolute path to the file to read. @@ -347,15 +338,11 @@ pub struct ReadToolParams { pub limit: Option, } -/// Writes content to the specified file in the project. -/// -/// In sessions with mcp__zed__Write always use it instead of Write as it will -/// allow the user to conveniently review changes. #[derive(Deserialize, JsonSchema, Debug)] pub struct WriteToolParams { - /// The absolute path of the file to write. - pub abs_path: PathBuf, - /// The full content to write. + /// Absolute path for new file + pub file_path: PathBuf, + /// File content pub content: String, } @@ -501,11 +488,10 @@ impl std::fmt::Display for GrepToolParams { } } -#[derive(Default, Deserialize, Serialize, JsonSchema, strum::Display, Debug)] +#[derive(Deserialize, Serialize, JsonSchema, strum::Display, Debug)] #[serde(rename_all = "snake_case")] pub enum TodoPriority { High, - #[default] Medium, Low, } @@ -540,13 +526,14 @@ impl Into for TodoStatus { #[derive(Deserialize, Serialize, JsonSchema, Debug)] pub struct Todo { + /// Unique identifier + pub id: String, /// Task description pub content: String, + /// Priority level of the todo + pub priority: TodoPriority, /// Current status of the todo pub status: TodoStatus, - /// Priority level of the todo - #[serde(default)] - pub priority: TodoPriority, } impl Into for Todo { diff --git a/crates/agent_servers/src/claude/write_tool.rs b/crates/agent_servers/src/claude/write_tool.rs deleted file mode 100644 index 39479a9c38..0000000000 --- a/crates/agent_servers/src/claude/write_tool.rs +++ /dev/null @@ -1,59 +0,0 @@ -use acp_thread::AcpThread; -use anyhow::Result; -use context_server::{ - listener::{McpServerTool, ToolResponse}, - types::ToolAnnotations, -}; -use gpui::{AsyncApp, WeakEntity}; - -use crate::tools::WriteToolParams; - -#[derive(Clone)] -pub struct WriteTool { - thread_rx: watch::Receiver>, -} - -impl WriteTool { - pub fn new(thread_rx: watch::Receiver>) -> Self { - Self { thread_rx } - } -} - -impl McpServerTool for WriteTool { - type Input = WriteToolParams; - type Output = (); - - const NAME: &'static str = "Write"; - - fn annotations(&self) -> ToolAnnotations { - ToolAnnotations { - title: Some("Write 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> { - let mut thread_rx = self.thread_rx.clone(); - let Some(thread) = thread_rx.recv().await?.upgrade() else { - anyhow::bail!("Thread closed"); - }; - - thread - .update(cx, |thread, cx| { - thread.write_text_file(input.abs_path, input.content, cx) - })? - .await?; - - Ok(ToolResponse { - content: vec![], - structured_content: (), - }) - } -} diff --git a/crates/agent_servers/src/codex.rs b/crates/agent_servers/src/codex.rs new file mode 100644 index 0000000000..712c333221 --- /dev/null +++ b/crates/agent_servers/src/codex.rs @@ -0,0 +1,319 @@ +use agent_client_protocol as acp; +use anyhow::anyhow; +use collections::HashMap; +use context_server::listener::McpServerTool; +use context_server::types::requests; +use context_server::{ContextServer, ContextServerCommand, ContextServerId}; +use futures::channel::{mpsc, oneshot}; +use project::Project; +use settings::SettingsStore; +use smol::stream::StreamExt as _; +use std::cell::RefCell; +use std::rc::Rc; +use std::{path::Path, sync::Arc}; +use util::ResultExt; + +use anyhow::{Context, Result}; +use gpui::{App, AppContext as _, AsyncApp, Entity, Task, WeakEntity}; + +use crate::mcp_server::ZedMcpServer; +use crate::{AgentServer, AgentServerCommand, AllAgentServersSettings, mcp_server}; +use acp_thread::{AcpThread, AgentConnection}; + +#[derive(Clone)] +pub struct Codex; + +impl AgentServer for Codex { + fn name(&self) -> &'static str { + "Codex" + } + + fn empty_state_headline(&self) -> &'static str { + "Welcome to Codex" + } + + fn empty_state_message(&self) -> &'static str { + "What can I help with?" + } + + fn logo(&self) -> ui::IconName { + ui::IconName::AiOpenAi + } + + fn connect( + &self, + _root_dir: &Path, + project: &Entity, + cx: &mut App, + ) -> Task>> { + let project = project.clone(); + let working_directory = project.read(cx).active_project_directory(cx); + cx.spawn(async move |cx| { + let settings = cx.read_global(|settings: &SettingsStore, _| { + settings.get::(None).codex.clone() + })?; + + let Some(command) = + AgentServerCommand::resolve("codex", &["mcp"], settings, &project, cx).await + else { + anyhow::bail!("Failed to find codex binary"); + }; + + let client: Arc = ContextServer::stdio( + ContextServerId("codex-mcp-server".into()), + ContextServerCommand { + path: command.path, + args: command.args, + env: command.env, + }, + working_directory, + ) + .into(); + ContextServer::start(client.clone(), cx).await?; + + let (notification_tx, mut notification_rx) = mpsc::unbounded(); + client + .client() + .context("Failed to subscribe")? + .on_notification(acp::SESSION_UPDATE_METHOD_NAME, { + move |notification, _cx| { + let notification_tx = notification_tx.clone(); + log::trace!( + "ACP Notification: {}", + serde_json::to_string_pretty(¬ification).unwrap() + ); + + if let Some(notification) = + serde_json::from_value::(notification) + .log_err() + { + notification_tx.unbounded_send(notification).ok(); + } + } + }); + + let sessions = Rc::new(RefCell::new(HashMap::default())); + + let notification_handler_task = cx.spawn({ + let sessions = sessions.clone(); + async move |cx| { + while let Some(notification) = notification_rx.next().await { + CodexConnection::handle_session_notification( + notification, + sessions.clone(), + cx, + ) + } + } + }); + + let connection = CodexConnection { + client, + sessions, + _notification_handler_task: notification_handler_task, + }; + Ok(Rc::new(connection) as _) + }) + } +} + +struct CodexConnection { + client: Arc, + sessions: Rc>>, + _notification_handler_task: Task<()>, +} + +struct CodexSession { + thread: WeakEntity, + cancel_tx: Option>, + _mcp_server: ZedMcpServer, +} + +impl AgentConnection for CodexConnection { + fn name(&self) -> &'static str { + "Codex" + } + + fn new_thread( + self: Rc, + project: Entity, + cwd: &Path, + cx: &mut AsyncApp, + ) -> Task>> { + let client = self.client.client(); + let sessions = self.sessions.clone(); + let cwd = cwd.to_path_buf(); + cx.spawn(async move |cx| { + let client = client.context("MCP server is not initialized yet")?; + let (mut thread_tx, thread_rx) = watch::channel(WeakEntity::new_invalid()); + + let mcp_server = ZedMcpServer::new(thread_rx, cx).await?; + + let response = client + .request::(context_server::types::CallToolParams { + name: acp::NEW_SESSION_TOOL_NAME.into(), + arguments: Some(serde_json::to_value(acp::NewSessionArguments { + mcp_servers: [( + mcp_server::SERVER_NAME.to_string(), + mcp_server.server_config()?, + )] + .into(), + client_tools: acp::ClientTools { + request_permission: Some(acp::McpToolId { + mcp_server: mcp_server::SERVER_NAME.into(), + tool_name: mcp_server::RequestPermissionTool::NAME.into(), + }), + read_text_file: Some(acp::McpToolId { + mcp_server: mcp_server::SERVER_NAME.into(), + tool_name: mcp_server::ReadTextFileTool::NAME.into(), + }), + write_text_file: Some(acp::McpToolId { + mcp_server: mcp_server::SERVER_NAME.into(), + tool_name: mcp_server::WriteTextFileTool::NAME.into(), + }), + }, + cwd, + })?), + meta: None, + }) + .await?; + + if response.is_error.unwrap_or_default() { + return Err(anyhow!(response.text_contents())); + } + + let result = serde_json::from_value::( + response.structured_content.context("Empty response")?, + )?; + + let thread = + cx.new(|cx| AcpThread::new(self.clone(), project, result.session_id.clone(), cx))?; + + thread_tx.send(thread.downgrade())?; + + let session = CodexSession { + thread: thread.downgrade(), + cancel_tx: None, + _mcp_server: mcp_server, + }; + sessions.borrow_mut().insert(result.session_id, session); + + Ok(thread) + }) + } + + fn authenticate(&self, _cx: &mut App) -> Task> { + Task::ready(Err(anyhow!("Authentication not supported"))) + } + + fn prompt( + &self, + params: agent_client_protocol::PromptArguments, + cx: &mut App, + ) -> Task> { + let client = self.client.client(); + let sessions = self.sessions.clone(); + + cx.foreground_executor().spawn(async move { + let client = client.context("MCP server is not initialized yet")?; + + let (new_cancel_tx, cancel_rx) = oneshot::channel(); + { + let mut sessions = sessions.borrow_mut(); + let session = sessions + .get_mut(¶ms.session_id) + .context("Session not found")?; + session.cancel_tx.replace(new_cancel_tx); + } + + let result = client + .request_with::( + context_server::types::CallToolParams { + name: acp::PROMPT_TOOL_NAME.into(), + arguments: Some(serde_json::to_value(params)?), + meta: None, + }, + Some(cancel_rx), + None, + ) + .await; + + if let Err(err) = &result + && err.is::() + { + return Ok(()); + } + + let response = result?; + + if response.is_error.unwrap_or_default() { + return Err(anyhow!(response.text_contents())); + } + + Ok(()) + }) + } + + fn cancel(&self, session_id: &agent_client_protocol::SessionId, _cx: &mut App) { + let mut sessions = self.sessions.borrow_mut(); + + if let Some(cancel_tx) = sessions + .get_mut(session_id) + .and_then(|session| session.cancel_tx.take()) + { + cancel_tx.send(()).ok(); + } + } +} + +impl CodexConnection { + pub fn handle_session_notification( + notification: acp::SessionNotification, + threads: Rc>>, + cx: &mut AsyncApp, + ) { + let threads = threads.borrow(); + let Some(thread) = threads + .get(¬ification.session_id) + .and_then(|session| session.thread.upgrade()) + else { + log::error!( + "Thread not found for session ID: {}", + notification.session_id + ); + return; + }; + + thread + .update(cx, |thread, cx| { + thread.handle_session_update(notification.update, cx) + }) + .log_err(); + } +} + +impl Drop for CodexConnection { + fn drop(&mut self) { + self.client.stop().log_err(); + } +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use crate::AgentServerCommand; + use std::path::Path; + + crate::common_e2e_tests!(Codex, allow_option_id = "approve"); + + pub fn local_command() -> AgentServerCommand { + let cli_path = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../../codex/codex-rs/target/debug/codex"); + + AgentServerCommand { + path: cli_path, + args: vec![], + env: None, + } + } +} diff --git a/crates/agent_servers/src/custom.rs b/crates/agent_servers/src/custom.rs deleted file mode 100644 index 72823026d7..0000000000 --- a/crates/agent_servers/src/custom.rs +++ /dev/null @@ -1,63 +0,0 @@ -use crate::{AgentServerCommand, AgentServerSettings}; -use acp_thread::AgentConnection; -use anyhow::Result; -use gpui::{App, Entity, SharedString, Task}; -use project::Project; -use std::{path::Path, rc::Rc}; -use ui::IconName; - -/// A generic agent server implementation for custom user-defined agents -pub struct CustomAgentServer { - name: SharedString, - command: AgentServerCommand, -} - -impl CustomAgentServer { - pub fn new(name: SharedString, settings: &AgentServerSettings) -> Self { - Self { - name, - command: settings.command.clone(), - } - } -} - -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 empty_state_headline(&self) -> SharedString { - "No conversations yet".into() - } - - fn empty_state_message(&self) -> SharedString { - format!("Start a conversation with {}", self.name).into() - } - - fn connect( - &self, - root_dir: &Path, - _project: &Entity, - cx: &mut App, - ) -> Task>> { - let server_name = self.name(); - let command = self.command.clone(); - let root_dir = root_dir.to_path_buf(); - - cx.spawn(async move |mut cx| { - crate::acp::connect(server_name, command, &root_dir, &mut cx).await - }) - } - - fn into_any(self: Rc) -> Rc { - self - } -} diff --git a/crates/agent_servers/src/e2e_tests.rs b/crates/agent_servers/src/e2e_tests.rs index 42264b4b4f..e9c72eabc9 100644 --- a/crates/agent_servers/src/e2e_tests.rs +++ b/crates/agent_servers/src/e2e_tests.rs @@ -1,31 +1,24 @@ -use crate::AgentServer; -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; -use project::{FakeFs, Project}; 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: F, cx: &mut TestAppContext) -where - T: AgentServer + 'static, - F: AsyncFn(&Arc, &Entity, &mut TestAppContext) -> T, -{ - let fs = init_test(cx).await as Arc; - let project = Project::test(fs.clone(), [], cx).await; - let thread = new_test_thread( - server(&fs, &project, cx).await, - project.clone(), - "/private/tmp", - cx, - ) - .await; +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; thread .update(cx, |thread, cx| thread.send_raw("Hello from Zed!", cx)) @@ -49,12 +42,8 @@ where }); } -pub async fn test_path_mentions(server: F, cx: &mut TestAppContext) -where - T: AgentServer + 'static, - F: AsyncFn(&Arc, &Entity, &mut TestAppContext) -> T, -{ - let fs = init_test(cx).await as _; +pub async fn test_path_mentions(server: impl AgentServer + 'static, cx: &mut TestAppContext) { + let _fs = init_test(cx).await; let tempdir = tempfile::tempdir().unwrap(); std::fs::write( @@ -67,13 +56,7 @@ where ) .expect("failed to write file"); let project = Project::example([tempdir.path()], &mut cx.to_async()).await; - let thread = new_test_thread( - server(&fs, &project, cx).await, - project.clone(), - tempdir.path(), - cx, - ) - .await; + let thread = new_test_thread(server, project.clone(), tempdir.path(), cx).await; thread .update(cx, |thread, cx| { thread.send( @@ -127,25 +110,15 @@ where drop(tempdir); } -pub async fn test_tool_call(server: F, cx: &mut TestAppContext) -where - T: AgentServer + 'static, - F: AsyncFn(&Arc, &Entity, &mut TestAppContext) -> T, -{ - let fs = init_test(cx).await as _; +pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestAppContext) { + let _fs = init_test(cx).await; 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(&fs, &project, cx).await, - project.clone(), - "/private/tmp", - cx, - ) - .await; + let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await; thread .update(cx, |thread, cx| { @@ -161,9 +134,7 @@ where matches!( entry, AgentThreadEntry::ToolCall(ToolCall { - status: ToolCallStatus::Pending - | ToolCallStatus::InProgress - | ToolCallStatus::Completed, + status: ToolCallStatus::Allowed { .. }, .. }) ) @@ -179,23 +150,14 @@ where drop(tempdir); } -pub async fn test_tool_call_with_permission( - server: F, +pub async fn test_tool_call_with_confirmation( + server: impl AgentServer + 'static, allow_option_id: acp::PermissionOptionId, cx: &mut TestAppContext, -) where - T: AgentServer + 'static, - F: AsyncFn(&Arc, &Entity, &mut TestAppContext) -> T, -{ - let fs = init_test(cx).await as Arc; - 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 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; 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."#, @@ -250,9 +212,7 @@ pub async fn test_tool_call_with_permission( assert!(thread.entries().iter().any(|entry| matches!( entry, AgentThreadEntry::ToolCall(ToolCall { - status: ToolCallStatus::Pending - | ToolCallStatus::InProgress - | ToolCallStatus::Completed, + status: ToolCallStatus::Allowed { .. }, .. }) ))); @@ -263,9 +223,7 @@ pub async fn test_tool_call_with_permission( thread.read_with(cx, |thread, cx| { let AgentThreadEntry::ToolCall(ToolCall { content, - status: ToolCallStatus::Pending - | ToolCallStatus::InProgress - | ToolCallStatus::Completed, + status: ToolCallStatus::Allowed { .. }, .. }) = thread .entries() @@ -283,22 +241,12 @@ pub async fn test_tool_call_with_permission( }); } -pub async fn test_cancel(server: F, cx: &mut TestAppContext) -where - T: AgentServer + 'static, - F: AsyncFn(&Arc, &Entity, &mut TestAppContext) -> T, -{ - let fs = init_test(cx).await as Arc; +pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppContext) { + let fs = init_test(cx).await; - 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| { + let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await; + let thread = new_test_thread(server, 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."#, cx, @@ -337,8 +285,9 @@ where id.clone() }); - thread.update(cx, |thread, cx| thread.cancel(cx)).await; - thread.read_with(cx, |thread, _cx| { + let _ = thread.update(cx, |thread, cx| thread.cancel(cx)); + full_turn.await.unwrap(); + thread.read_with(cx, |thread, _| { let AgentThreadEntry::ToolCall(ToolCall { status: ToolCallStatus::Canceled, .. @@ -362,37 +311,6 @@ where }); } -pub async fn test_thread_drop(server: F, cx: &mut TestAppContext) -where - T: AgentServer + 'static, - F: AsyncFn(&Arc, &Entity, &mut TestAppContext) -> T, -{ - let fs = init_test(cx).await as Arc; - 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)) - .await - .unwrap(); - - thread.read_with(cx, |thread, _| { - assert!(thread.entries().len() >= 2, "Expected at least 2 entries"); - }); - - let weak_thread = thread.downgrade(); - drop(thread); - - cx.executor().run_until_parked(); - assert!(!weak_thread.is_upgradable()); -} - #[macro_export] macro_rules! common_e2e_tests { ($server:expr, allow_option_id = $allow_option_id:expr) => { @@ -419,8 +337,8 @@ macro_rules! common_e2e_tests { #[::gpui::test] #[cfg_attr(not(feature = "e2e"), ignore)] - async fn tool_call_with_permission(cx: &mut ::gpui::TestAppContext) { - $crate::e2e_tests::test_tool_call_with_permission( + async fn tool_call_with_confirmation(cx: &mut ::gpui::TestAppContext) { + $crate::e2e_tests::test_tool_call_with_confirmation( $server, ::agent_client_protocol::PermissionOptionId($allow_option_id.into()), cx, @@ -433,51 +351,33 @@ macro_rules! common_e2e_tests { async fn cancel(cx: &mut ::gpui::TestAppContext) { $crate::e2e_tests::test_cancel($server, cx).await; } - - #[::gpui::test] - #[cfg_attr(not(feature = "e2e"), ignore)] - async fn thread_drop(cx: &mut ::gpui::TestAppContext) { - $crate::e2e_tests::test_thread_drop($server, cx).await; - } } }; } -pub use common_e2e_tests; // Helpers pub async fn init_test(cx: &mut TestAppContext) -> Arc { - #[cfg(test)] - use settings::Settings; - env_logger::try_init().ok(); cx.update(|cx| { - let settings_store = settings::SettingsStore::test(cx); + let settings_store = SettingsStore::test(cx); cx.set_global(settings_store); Project::init_settings(cx); language::init(cx); - 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); crate::settings::init(cx); - #[cfg(test)] crate::AllAgentServersSettings::override_global( - crate::AllAgentServersSettings { - claude: Some(crate::AgentServerSettings { + AllAgentServersSettings { + claude: Some(AgentServerSettings { command: crate::claude::tests::local_command(), }), - gemini: Some(crate::AgentServerSettings { + gemini: Some(AgentServerSettings { command: crate::gemini::tests::local_command(), }), - custom: collections::HashMap::default(), + codex: Some(AgentServerSettings { + command: crate::codex::tests::local_command(), + }), }, cx, ); @@ -499,9 +399,12 @@ pub async fn new_test_thread( .await .unwrap(); - cx.update(|cx| connection.new_thread(project.clone(), current_dir.as_ref(), cx)) + let thread = connection + .new_thread(project.clone(), current_dir.as_ref(), &mut cx.to_async()) .await - .unwrap() + .unwrap(); + + thread } pub async fn run_until_first_tool_call( @@ -539,7 +442,7 @@ pub fn get_zed_path() -> PathBuf { while zed_path .file_name() - .is_none_or(|name| name.to_string_lossy() != "debug") + .map_or(true, |name| name.to_string_lossy() != "debug") { if !zed_path.pop() { panic!("Could not find target directory"); diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index 5d6a70fa64..a97ff3f462 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -1,13 +1,17 @@ +use anyhow::anyhow; +use std::cell::RefCell; +use std::path::Path; use std::rc::Rc; -use std::{any::Any, path::Path}; +use util::ResultExt as _; -use crate::{AgentServer, AgentServerCommand}; -use acp_thread::{AgentConnection, LoadError}; -use anyhow::Result; -use gpui::{App, Entity, SharedString, Task}; -use language_models::provider::google::GoogleLanguageModelProvider; +use crate::{AgentServer, AgentServerCommand, AgentServerVersion}; +use acp_thread::{AgentConnection, LoadError, OldAcpAgentConnection, OldAcpClientDelegate}; +use agentic_coding_protocol as acp_old; +use anyhow::{Context as _, Result}; +use gpui::{AppContext as _, AsyncApp, Entity, Task, WeakEntity}; use project::Project; use settings::SettingsStore; +use ui::App; use crate::AllAgentServersSettings; @@ -17,20 +21,16 @@ pub struct Gemini; const ACP_ARG: &str = "--experimental-acp"; impl AgentServer for Gemini { - fn telemetry_id(&self) -> &'static str { - "gemini-cli" + fn name(&self) -> &'static str { + "Gemini" } - fn name(&self) -> SharedString { - "Gemini CLI".into() + fn empty_state_headline(&self) -> &'static str { + "Welcome to Gemini" } - fn empty_state_headline(&self) -> SharedString { - self.name() - } - - fn empty_state_message(&self) -> SharedString { - "Ask questions, edit files, run commands".into() + fn empty_state_message(&self) -> &'static str { + "Ask questions, edit files, run commands.\nBe specific for the best results." } fn logo(&self) -> ui::IconName { @@ -43,79 +43,143 @@ impl AgentServer for Gemini { project: &Entity, cx: &mut App, ) -> Task>> { - let project = project.clone(); let root_dir = root_dir.to_path_buf(); - let server_name = self.name(); + let project = project.clone(); + let this = self.clone(); + let name = self.name(); + cx.spawn(async move |cx| { - let settings = cx.read_global(|settings: &SettingsStore, _| { - settings.get::(None).gemini.clone() - })?; + let command = this.command(&project, cx).await?; - let Some(mut command) = - AgentServerCommand::resolve("gemini", &[ACP_ARG], None, settings, &project, cx).await - else { - return Err(LoadError::NotInstalled { - error_message: "Failed to find Gemini CLI binary".into(), - install_message: "Install Gemini CLI".into(), - install_command: Self::install_command().into(), - }.into()); - }; + 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()?; - if let Some(api_key)= cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() { - command.env.get_or_insert_default().insert("GEMINI_API_KEY".to_owned(), api_key.key); - } + let stdin = child.stdin.take().unwrap(); + let stdout = child.stdout.take().unwrap(); - 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 foreground_executor = cx.foreground_executor().clone(); - let help_fut = util::command::new_smol_command(&command.path) - .args(command.args.iter()) - .arg("--help") - .kill_on_drop(true) - .output(); + let thread_rc = Rc::new(RefCell::new(WeakEntity::new_invalid())); - let (version_output, help_output) = futures::future::join(version_fut, help_fut).await; + 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 current_version = String::from_utf8(version_output?.stdout)?; - let supported = String::from_utf8(help_output?.stdout)?.contains(ACP_ARG); + let io_task = cx.background_spawn(async move { + io_fut.await.log_err(); + }); - if !supported { - return Err(LoadError::Unsupported { - error_message: format!( - "Your installed version of Gemini CLI ({}, version {}) doesn't support the Agentic Coding Protocol (ACP).", - command.path.to_string_lossy(), - current_version - ).into(), - upgrade_message: "Upgrade Gemini CLI to latest".into(), - upgrade_command: Self::upgrade_command().into(), - }.into()) - } - } - result + 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) => { + if let Some(AgentServerVersion::Unsupported { + error_message, + upgrade_message, + upgrade_command, + }) = this.version(&command).await.log_err() + { + Err(anyhow!(LoadError::Unsupported { + error_message, + upgrade_message, + upgrade_command + })) + } else { + Err(anyhow!(LoadError::Exited(result.code().unwrap_or(-127)))) + } + } + }; + drop(io_task); + result + }); + + let connection: Rc = Rc::new(OldAcpAgentConnection { + name, + connection, + child_status, + current_thread: thread_rc, + }); + + Ok(connection) }) } - - fn into_any(self: Rc) -> Rc { - self - } } impl Gemini { - pub fn binary_name() -> &'static str { - "gemini" + async fn command( + &self, + project: &Entity, + cx: &mut AsyncApp, + ) -> Result { + let settings = cx.read_global(|settings: &SettingsStore, _| { + settings.get::(None).gemini.clone() + })?; + + if let Some(command) = + AgentServerCommand::resolve("gemini", &[ACP_ARG], settings, &project, cx).await + { + return Ok(command); + }; + + let (fs, node_runtime) = project.update(cx, |project, _| { + (project.fs().clone(), project.node_runtime().cloned()) + })?; + let node_runtime = node_runtime.context("gemini not found on path")?; + + let directory = ::paths::agent_servers_dir().join("gemini"); + fs.create_dir(&directory).await?; + node_runtime + .npm_install_packages(&directory, &[("@google/gemini-cli", "latest")]) + .await?; + let path = directory.join("node_modules/.bin/gemini"); + + Ok(AgentServerCommand { + path, + args: vec![ACP_ARG.into()], + env: None, + }) } - pub fn install_command() -> &'static str { - "npm install -g @google/gemini-cli@preview" - } + async fn version(&self, command: &AgentServerCommand) -> Result { + let version_fut = util::command::new_smol_command(&command.path) + .args(command.args.iter()) + .arg("--version") + .kill_on_drop(true) + .output(); - pub fn upgrade_command() -> &'static str { - "npm install -g @google/gemini-cli@preview" + let help_fut = util::command::new_smol_command(&command.path) + .args(command.args.iter()) + .arg("--help") + .kill_on_drop(true) + .output(); + + 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 { + Ok(AgentServerVersion::Supported) + } else { + Ok(AgentServerVersion::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(), + }) + } } } @@ -125,7 +189,7 @@ pub(crate) mod tests { use crate::AgentServerCommand; use std::path::Path; - crate::common_e2e_tests!(async |_, _, _| Gemini, allow_option_id = "proceed_once"); + crate::common_e2e_tests!(Gemini, allow_option_id = "0"); pub fn local_command() -> AgentServerCommand { let cli_path = Path::new(env!("CARGO_MANIFEST_DIR")) @@ -135,7 +199,7 @@ pub(crate) mod tests { AgentServerCommand { path: "node".into(), - args: vec![cli_path], + args: vec![cli_path, ACP_ARG.into()], env: None, } } diff --git a/crates/agent_servers/src/mcp_server.rs b/crates/agent_servers/src/mcp_server.rs new file mode 100644 index 0000000000..055b89dfe2 --- /dev/null +++ b/crates/agent_servers/src/mcp_server.rs @@ -0,0 +1,207 @@ +use acp_thread::AcpThread; +use agent_client_protocol as acp; +use anyhow::Result; +use context_server::listener::{McpServerTool, ToolResponse}; +use context_server::types::{ + Implementation, InitializeParams, InitializeResponse, ProtocolVersion, ServerCapabilities, + ToolsCapabilities, requests, +}; +use futures::channel::oneshot; +use gpui::{App, AsyncApp, Task, WeakEntity}; +use indoc::indoc; + +pub struct ZedMcpServer { + server: context_server::listener::McpServer, +} + +pub const SERVER_NAME: &str = "zed"; + +impl ZedMcpServer { + pub async fn new( + thread_rx: watch::Receiver>, + cx: &AsyncApp, + ) -> Result { + let mut mcp_server = context_server::listener::McpServer::new(cx).await?; + mcp_server.handle_request::(Self::handle_initialize); + + mcp_server.add_tool(RequestPermissionTool { + thread_rx: thread_rx.clone(), + }); + mcp_server.add_tool(ReadTextFileTool { + thread_rx: thread_rx.clone(), + }); + mcp_server.add_tool(WriteTextFileTool { + thread_rx: thread_rx.clone(), + }); + + Ok(Self { server: mcp_server }) + } + + pub fn server_config(&self) -> Result { + #[cfg(not(test))] + let zed_path = anyhow::Context::context( + std::env::current_exe(), + "finding current executable path for use in mcp_server", + )?; + + #[cfg(test)] + let zed_path = crate::e2e_tests::get_zed_path(); + + Ok(acp::McpServerConfig { + command: zed_path, + args: vec![ + "--nc".into(), + self.server.socket_path().display().to_string(), + ], + env: None, + }) + } + + fn handle_initialize(_: InitializeParams, cx: &App) -> Task> { + 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, + }) + }) + } +} + +// Tools + +#[derive(Clone)] +pub struct RequestPermissionTool { + thread_rx: watch::Receiver>, +} + +impl McpServerTool for RequestPermissionTool { + type Input = acp::RequestPermissionArguments; + type Output = acp::RequestPermissionOutput; + + const NAME: &'static str = "Confirmation"; + + fn description(&self) -> &'static str { + indoc! {" + Request permission for tool calls. + + This tool is meant to be called programmatically by the agent loop, not the LLM. + "} + } + + async fn run( + &self, + input: Self::Input, + cx: &mut AsyncApp, + ) -> Result> { + let mut thread_rx = self.thread_rx.clone(); + let Some(thread) = thread_rx.recv().await?.upgrade() else { + anyhow::bail!("Thread closed"); + }; + + let result = thread + .update(cx, |thread, cx| { + thread.request_tool_call_permission(input.tool_call, input.options, cx) + })? + .await; + + let outcome = match result { + Ok(option_id) => acp::RequestPermissionOutcome::Selected { option_id }, + Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Canceled, + }; + + Ok(ToolResponse { + content: vec![], + structured_content: acp::RequestPermissionOutput { outcome }, + }) + } +} + +#[derive(Clone)] +pub struct ReadTextFileTool { + thread_rx: watch::Receiver>, +} + +impl McpServerTool for ReadTextFileTool { + type Input = acp::ReadTextFileArguments; + type Output = acp::ReadTextFileOutput; + + const NAME: &'static str = "Read"; + + fn description(&self) -> &'static str { + "Reads the content of the given file in the project including unsaved changes." + } + + async fn run( + &self, + input: Self::Input, + cx: &mut AsyncApp, + ) -> Result> { + 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.path, input.line, input.limit, false, cx) + })? + .await?; + + Ok(ToolResponse { + content: vec![], + structured_content: acp::ReadTextFileOutput { content }, + }) + } +} + +#[derive(Clone)] +pub struct WriteTextFileTool { + thread_rx: watch::Receiver>, +} + +impl McpServerTool for WriteTextFileTool { + type Input = acp::WriteTextFileArguments; + type Output = (); + + const NAME: &'static str = "Write"; + + fn description(&self) -> &'static str { + "Write to a file replacing its contents" + } + + async fn run( + &self, + input: Self::Input, + cx: &mut AsyncApp, + ) -> Result> { + let mut thread_rx = self.thread_rx.clone(); + let Some(thread) = thread_rx.recv().await?.upgrade() else { + anyhow::bail!("Thread closed"); + }; + + thread + .update(cx, |thread, cx| { + thread.write_text_file(input.path, input.content, cx) + })? + .await?; + + Ok(ToolResponse { + content: vec![], + structured_content: (), + }) + } +} diff --git a/crates/agent_servers/src/settings.rs b/crates/agent_servers/src/settings.rs index 96ac6e3cbe..aeb34a5e61 100644 --- a/crates/agent_servers/src/settings.rs +++ b/crates/agent_servers/src/settings.rs @@ -1,7 +1,6 @@ use crate::AgentServerCommand; use anyhow::Result; -use collections::HashMap; -use gpui::{App, SharedString}; +use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsSources}; @@ -14,13 +13,10 @@ pub fn init(cx: &mut App) { pub struct AllAgentServersSettings { pub gemini: Option, pub claude: Option, - - /// Custom agent servers configured by the user - #[serde(flatten)] - pub custom: HashMap, + pub codex: Option, } -#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)] +#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug)] pub struct AgentServerSettings { #[serde(flatten)] pub command: AgentServerCommand, @@ -37,7 +33,7 @@ impl settings::Settings for AllAgentServersSettings { for AllAgentServersSettings { gemini, claude, - custom, + codex, } in sources.defaults_and_customizations() { if gemini.is_some() { @@ -46,13 +42,8 @@ impl settings::Settings for AllAgentServersSettings { 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()); - } + if codex.is_some() { + settings.codex = codex.clone(); } } diff --git a/crates/agent_settings/src/agent_profile.rs b/crates/agent_settings/src/agent_profile.rs index 04fdd4a753..a6b8633b34 100644 --- a/crates/agent_settings/src/agent_profile.rs +++ b/crates/agent_settings/src/agent_profile.rs @@ -48,20 +48,6 @@ pub struct AgentProfileSettings { pub context_servers: IndexMap, ContextServerPreset>, } -impl AgentProfileSettings { - pub fn is_tool_enabled(&self, tool_name: &str) -> bool { - self.tools.get(tool_name) == Some(&true) - } - - pub fn is_context_server_tool_enabled(&self, server_id: &str, tool_name: &str) -> bool { - self.enable_all_context_servers - || self - .context_servers - .get(server_id) - .is_some_and(|preset| preset.tools.get(tool_name) == Some(&true)) - } -} - #[derive(Debug, Clone, Default)] pub struct ContextServerPreset { pub tools: IndexMap, bool>, diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index ed1ed2b898..4e872c78d7 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -13,11 +13,6 @@ 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); } @@ -118,15 +113,15 @@ pub struct LanguageModelParameters { impl LanguageModelParameters { pub fn matches(&self, model: &Arc) -> bool { - if let Some(provider) = &self.provider - && provider.0 != model.provider_id().0 - { - return false; + if let Some(provider) = &self.provider { + if provider.0 != model.provider_id().0 { + return false; + } } - if let Some(setting_model) = &self.model - && *setting_model != model.id().0 - { - return false; + if let Some(setting_model) = &self.model { + if *setting_model != model.id().0 { + return false; + } } true } @@ -311,7 +306,7 @@ pub struct AgentSettingsContent { /// /// Default: true expand_terminal_card: Option, - /// Whether to always use cmd-enter (or ctrl-enter on Linux or Windows) to send messages in the agent panel. + /// Whether to always use cmd-enter (or ctrl-enter on Linux) to send messages in the agent panel. /// /// Default: false use_modifier_to_send: Option, @@ -444,6 +439,10 @@ impl Settings for AgentSettings { &mut settings.inline_alternatives, value.inline_alternatives.clone(), ); + merge( + &mut settings.always_allow_tool_actions, + value.always_allow_tool_actions, + ); merge( &mut settings.notify_when_agent_waiting, value.notify_when_agent_waiting, @@ -505,19 +504,6 @@ impl Settings for AgentSettings { } } - 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!" - ); - - // For security reasons, only trust the user's global settings for whether to always allow tool actions. - // If this could be overridden locally, an attacker could (e.g. by committing to source control and - // convincing you to switch branches) modify your project-local settings to disable the agent's safety checks. - settings.always_allow_tool_actions = sources - .user - .and_then(|setting| setting.always_allow_tool_actions) - .unwrap_or(false); - Ok(settings) } diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index 6b0979ee69..95fd2b1757 100644 --- a/crates/agent_ui/Cargo.toml +++ b/crates/agent_ui/Cargo.toml @@ -17,10 +17,8 @@ test-support = ["gpui/test-support", "language/test-support"] [dependencies] acp_thread.workspace = true -action_log.workspace = true agent-client-protocol.workspace = true agent.workspace = true -agent2.workspace = true agent_servers.workspace = true agent_settings.workspace = true ai_onboarding.workspace = true @@ -50,6 +48,7 @@ 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 @@ -67,7 +66,6 @@ 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 @@ -93,7 +91,6 @@ time.workspace = true time_format.workspace = true ui.workspace = true ui_input.workspace = true -url.workspace = true urlencoding.workspace = true util.workspace = true uuid.workspace = true @@ -103,13 +100,8 @@ 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 diff --git a/crates/agent_ui/src/acp.rs b/crates/agent_ui/src/acp.rs index 6f228b91d6..cc476b1a86 100644 --- a/crates/agent_ui/src/acp.rs +++ b/crates/agent_ui/src/acp.rs @@ -1,12 +1,6 @@ mod completion_provider; -mod entry_view_state; -mod message_editor; -mod model_selector; -mod model_selector_popover; -mod thread_history; +mod message_history; mod thread_view; -pub use model_selector::AcpModelSelector; -pub use model_selector_popover::AcpModelSelectorPopover; -pub use thread_history::*; +pub use message_history::MessageHistory; pub use thread_view::AcpThreadView; diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index 5b40967069..fca4ae0300 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -1,222 +1,101 @@ -use std::cell::Cell; use std::ops::Range; -use std::rc::Rc; +use std::path::Path; use std::sync::Arc; use std::sync::atomic::AtomicBool; -use acp_thread::MentionUri; -use agent_client_protocol as acp; -use agent2::{HistoryEntry, HistoryStore}; use anyhow::Result; +use collections::HashMap; +use editor::display_map::CreaseId; use editor::{CompletionProvider, Editor, ExcerptId}; -use fuzzy::{StringMatch, StringMatchCandidate}; +use file_icons::FileIcons; use gpui::{App, Entity, Task, WeakEntity}; use language::{Buffer, CodeLabel, HighlightId}; use lsp::CompletionContext; -use project::{ - Completion, CompletionIntent, CompletionResponse, Project, ProjectPath, Symbol, WorktreeId, -}; -use prompt_store::PromptStore; +use parking_lot::Mutex; +use project::{Completion, CompletionIntent, CompletionResponse, ProjectPath, WorktreeId}; use rope::Point; -use text::{Anchor, ToPoint as _}; +use text::{Anchor, ToPoint}; use ui::prelude::*; use workspace::Workspace; -use crate::AgentPanel; -use crate::acp::message_editor::MessageEditor; -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::{ - ContextPickerAction, ContextPickerEntry, ContextPickerMode, selection_ranges, -}; +use crate::context_picker::MentionLink; +use crate::context_picker::file_context_picker::{extract_file_name_and_directory, search_files}; -pub(crate) enum Match { - File(FileMatch), - Symbol(SymbolMatch), - Thread(HistoryEntry), - RecentThread(HistoryEntry), - Fetch(SharedString), - Rules(RulesContextEntry), - Entry(EntryMatch), +#[derive(Default)] +pub struct MentionSet { + paths_by_crease_id: HashMap, } -pub struct EntryMatch { - mat: Option, - entry: ContextPickerEntry, -} +impl MentionSet { + pub fn insert(&mut self, crease_id: CreaseId, path: ProjectPath) { + self.paths_by_crease_id.insert(crease_id, path); + } -impl Match { - pub fn score(&self) -> f64 { - match self { - 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., - } + pub fn path_for_crease_id(&self, crease_id: CreaseId) -> Option { + self.paths_by_crease_id.get(&crease_id).cloned() + } + + pub fn drain(&mut self) -> impl Iterator { + self.paths_by_crease_id.drain().map(|(id, _)| id) } } pub struct ContextPickerCompletionProvider { - message_editor: WeakEntity, workspace: WeakEntity, - history_store: Entity, - prompt_store: Option>, - prompt_capabilities: Rc>, + editor: WeakEntity, + mention_set: Arc>, } impl ContextPickerCompletionProvider { pub fn new( - message_editor: WeakEntity, + mention_set: Arc>, workspace: WeakEntity, - history_store: Entity, - prompt_store: Option>, - prompt_capabilities: Rc>, + editor: WeakEntity, ) -> Self { Self { - message_editor, + mention_set, workspace, - history_store, - prompt_store, - prompt_capabilities, + editor, } } - fn completion_for_entry( - entry: ContextPickerEntry, - source_range: Range, - message_editor: WeakEntity, - workspace: &Entity, - cx: &mut App, - ) -> Option { - match entry { - ContextPickerEntry::Mode(mode) => Some(Completion { - replace_range: source_range, - new_text: format!("@{} ", mode.keyword()), - label: CodeLabel::plain(mode.label().to_string(), None), - icon_path: Some(mode.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(Arc::new(|_, _, _| true)), - }), - ContextPickerEntry::Action(action) => { - Self::completion_for_action(action, source_range, message_editor, workspace, cx) - } - } - } - - fn completion_for_thread( - thread_entry: HistoryEntry, - source_range: Range, - recent: bool, - editor: WeakEntity, - cx: &mut App, - ) -> Completion { - let uri = thread_entry.mention_uri(); - - let icon_for_completion = if recent { - IconName::HistoryRerun.path().into() - } else { - uri.icon_path(cx) - }; - - let new_text = format!("{} ", uri.as_link()); - - let new_text_len = new_text.len(); - Completion { - replace_range: source_range.clone(), - new_text, - label: CodeLabel::plain(thread_entry.title().to_string(), None), - documentation: None, - insert_text_mode: None, - source: project::CompletionSource::Custom, - icon_path: Some(icon_for_completion), - confirm: Some(confirm_completion_callback( - thread_entry.title().clone(), - source_range.start, - new_text_len - 1, - editor, - uri, - )), - } - } - - fn completion_for_rules( - rule: RulesContextEntry, - source_range: Range, - editor: WeakEntity, - cx: &mut App, - ) -> Completion { - let uri = MentionUri::Rule { - id: rule.prompt_id.into(), - name: rule.title.to_string(), - }; - 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, - label: CodeLabel::plain(rule.title.to_string(), None), - documentation: None, - insert_text_mode: None, - source: project::CompletionSource::Custom, - icon_path: Some(icon_path), - confirm: Some(confirm_completion_callback( - rule.title, - source_range.start, - new_text_len - 1, - editor, - uri, - )), - } - } - - pub(crate) fn completion_for_path( + fn completion_for_path( project_path: ProjectPath, path_prefix: &str, is_recent: bool, is_directory: bool, + excerpt_id: ExcerptId, source_range: Range, - message_editor: WeakEntity, - project: Entity, - cx: &mut App, - ) -> Option { + editor: Entity, + mention_set: Arc>, + cx: &App, + ) -> Completion { let (file_name, directory) = - crate::context_picker::file_context_picker::extract_file_name_and_directory( - &project_path.path, - path_prefix, - ); + extract_file_name_and_directory(&project_path.path, path_prefix); let label = build_code_label_for_full_path(&file_name, directory.as_ref().map(|s| s.as_ref()), cx); - - let abs_path = project.read(cx).absolute_path(&project_path, cx)?; - - let uri = if is_directory { - MentionUri::Directory { abs_path } + let full_path = if let Some(directory) = directory { + format!("{}{}", directory, file_name) } else { - MentionUri::File { abs_path } + file_name.to_string() }; - let crease_icon_path = uri.icon_path(cx); + let crease_icon_path = if is_directory { + FileIcons::get_folder_icon(false, cx).unwrap_or_else(|| IconName::Folder.path().into()) + } else { + FileIcons::get_icon(Path::new(&full_path), cx) + .unwrap_or_else(|| IconName::File.path().into()) + }; let completion_icon_path = if is_recent { IconName::HistoryRerun.path().into() } else { - crease_icon_path + crease_icon_path.clone() }; - let new_text = format!("{} ", uri.as_link()); + let new_text = format!("{} ", MentionLink::for_file(&file_name, &full_path)); let new_text_len = new_text.len(); - Some(Completion { + Completion { replace_range: source_range.clone(), new_text, label, @@ -225,409 +104,28 @@ impl ContextPickerCompletionProvider { icon_path: Some(completion_icon_path), insert_text_mode: None, confirm: Some(confirm_completion_callback( + crease_icon_path, file_name, + project_path, + excerpt_id, source_range.start, new_text_len - 1, - message_editor, - uri, + editor, + mention_set, )), - }) - } - - fn completion_for_symbol( - symbol: Symbol, - source_range: Range, - message_editor: WeakEntity, - workspace: Entity, - cx: &mut App, - ) -> Option { - let project = workspace.read(cx).project().clone(); - - let label = CodeLabel::plain(symbol.name.clone(), None); - - let abs_path = project.read(cx).absolute_path(&symbol.path, cx)?; - let uri = MentionUri::Symbol { - abs_path, - name: symbol.name.clone(), - 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(icon_path), - insert_text_mode: None, - confirm: Some(confirm_completion_callback( - symbol.name.into(), - source_range.start, - new_text_len - 1, - message_editor, - uri, - )), - }) - } - - fn completion_for_fetch( - source_range: Range, - url_to_fetch: SharedString, - message_editor: WeakEntity, - cx: &mut App, - ) -> Option { - 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.clone(), - label: CodeLabel::plain(url_to_fetch.to_string(), None), - documentation: None, - source: project::CompletionSource::Custom, - icon_path: Some(icon_path), - insert_text_mode: None, - confirm: Some(confirm_completion_callback( - url_to_fetch.to_string().into(), - source_range.start, - new_text.len() - 1, - message_editor, - mention_uri, - )), - }) - } - - pub(crate) fn completion_for_action( - action: ContextPickerAction, - source_range: Range, - message_editor: WeakEntity, - workspace: &Entity, - cx: &mut App, - ) -> Option { - 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::>(); - - 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.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 search( - &self, - mode: Option, - query: String, - cancellation_flag: Arc, - cx: &mut App, - ) -> Task> { - let Some(workspace) = self.workspace.upgrade() else { - 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() - }) - } - - 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() - }) - } - - 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() - }) - } - - 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) = 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::>() - }) - } else { - Task::ready(Vec::new()) - } - } - - None if query.is_empty() => { - let mut matches = self.recent_context_picker_entries(&workspace, cx); - - 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(); - - let search_files_task = - search_files(query.clone(), cancellation_flag, &workspace, 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::>(); - - cx.background_spawn(async move { - let mut matches = search_files_task - .await - .into_iter() - .map(Match::File) - .collect::>(); - - 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, - cx: &mut App, - ) -> Vec { - 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::(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, - }) - }) - }), - ); - - 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::>(); - - recent.extend(threads.into_iter().map(Match::RecentThread)); - } - - recent - } - - fn available_context_picker_entries( - &self, - workspace: &Entity, - cx: &mut App, - ) -> Vec { - 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::()) - .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(&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(); @@ -638,7 +136,7 @@ fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: impl CompletionProvider for ContextPickerCompletionProvider { fn completions( &self, - _excerpt_id: ExcerptId, + excerpt_id: ExcerptId, buffer: &Entity, buffer_position: Anchor, _trigger: CompletionContext, @@ -651,11 +149,7 @@ impl CompletionProvider for ContextPickerCompletionProvider { 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( - self.prompt_capabilities.get().embedded_context, - line, - offset_to_line, - ) + MentionCompletion::try_parse(line, offset_to_line) }); let Some(state) = state else { return Task::ready(Ok(Vec::new())); @@ -665,88 +159,44 @@ impl CompletionProvider for ContextPickerCompletionProvider { 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(); - - let MentionCompletion { mode, argument, .. } = state; + let editor = self.editor.clone(); + let mention_set = self.mention_set.clone(); + let MentionCompletion { argument, .. } = state; let query = argument.unwrap_or_else(|| "".to_string()); - let search_task = self.search(mode, query, Arc::::default(), cx); + let search_task = search_files(query.clone(), Arc::::default(), &workspace, cx); cx.spawn(async move |_, cx| { let matches = search_task.await; + let Some(editor) = editor.upgrade() else { + return Ok(Vec::new()); + }; 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(), - }; + .map(|mat| { + let path_match = &mat.mat; + let project_path = ProjectPath { + worktree_id: WorktreeId::from_usize(path_match.worktree_id), + path: path_match.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, + Self::completion_for_path( + project_path, + &path_match.path_prefix, + mat.is_recent, + path_match.is_dir, + excerpt_id, source_range.clone(), editor.clone(), - workspace.clone(), + mention_set.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() })?; @@ -775,16 +225,12 @@ impl CompletionProvider for ContextPickerCompletionProvider { let offset_to_line = buffer.point_to_offset(line_start); let mut lines = buffer.text_for_range(line_start..position).lines(); if let Some(line) = lines.next() { - MentionCompletion::try_parse( - self.prompt_capabilities.get().embedded_context, - line, - offset_to_line, - ) - .map(|completion| { - completion.source_range.start <= offset_to_line + position.column as usize - && completion.source_range.end >= offset_to_line + position.column as usize - }) - .unwrap_or(false) + MentionCompletion::try_parse(line, offset_to_line) + .map(|completion| { + completion.source_range.start <= offset_to_line + position.column as usize + && completion.source_range.end >= offset_to_line + position.column as usize + }) + .unwrap_or(false) } else { false } @@ -799,69 +245,36 @@ impl CompletionProvider for ContextPickerCompletionProvider { } } -pub(crate) fn search_threads( - query: String, - cancellation_flag: Arc, - history_store: &Entity, - cx: &mut App, -) -> Task> { - let threads = history_store.read(cx).entries().collect(); - if query.is_empty() { - return Task::ready(threads); - } - - let executor = cx.background_executor().clone(); - cx.background_spawn(async move { - let candidates = threads - .iter() - .enumerate() - .map(|(id, thread)| StringMatchCandidate::new(id, thread.title())) - .collect::>(); - let matches = fuzzy::match_strings( - &candidates, - &query, - false, - true, - 100, - &cancellation_flag, - executor, - ) - .await; - - matches - .into_iter() - .map(|mat| threads[mat.candidate_id].clone()) - .collect() - }) -} - fn confirm_completion_callback( + crease_icon_path: SharedString, crease_text: SharedString, + project_path: ProjectPath, + excerpt_id: ExcerptId, start: Anchor, content_len: usize, - message_editor: WeakEntity, - mention_uri: MentionUri, + editor: Entity, + mention_set: Arc>, ) -> Arc bool + Send + Sync> { Arc::new(move |_, window, cx| { - let message_editor = message_editor.clone(); let crease_text = crease_text.clone(); - let mention_uri = mention_uri.clone(); + let crease_icon_path = crease_icon_path.clone(); + let editor = editor.clone(); + let project_path = project_path.clone(); + let mention_set = mention_set.clone(); window.defer(cx, move |window, cx| { - message_editor - .clone() - .update(cx, |message_editor, cx| { - message_editor - .confirm_completion( - crease_text, - start, - content_len, - mention_uri, - window, - cx, - ) - .detach(); - }) - .ok(); + let crease_id = crate::context_picker::insert_crease_for_mention( + excerpt_id, + start, + content_len, + crease_text.clone(), + crease_icon_path, + editor.clone(), + window, + cx, + ); + if let Some(crease_id) = crease_id { + mention_set.lock().insert(crease_id, project_path); + } }); false }) @@ -870,12 +283,11 @@ fn confirm_completion_callback( #[derive(Debug, Default, PartialEq)] struct MentionCompletion { source_range: Range, - mode: Option, argument: Option, } impl MentionCompletion { - fn try_parse(allow_non_file_mentions: bool, line: &str, offset_to_line: usize) -> Option { + fn try_parse(line: &str, offset_to_line: usize) -> Option { let last_mention_start = line.rfind('@')?; if last_mention_start >= line.len() { return Some(Self::default()); @@ -884,45 +296,23 @@ impl MentionCompletion { && line .chars() .nth(last_mention_start - 1) - .is_some_and(|c| !c.is_whitespace()) + .map_or(false, |c| !c.is_whitespace()) { return None; } let rest_of_line = &line[last_mention_start + 1..]; - - let mut mode = None; let mut argument = None; let mut parts = rest_of_line.split_whitespace(); let mut end = last_mention_start + 1; - if let Some(mode_text) = parts.next() { - end += mode_text.len(); - - if let Some(parsed_mode) = ContextPickerMode::try_from(mode_text).ok() - && (allow_non_file_mentions || matches!(parsed_mode, ContextPickerMode::File)) - { - mode = Some(parsed_mode); - } else { - argument = Some(mode_text.to_string()); - } - match rest_of_line[mode_text.len()..].find(|c: char| !c.is_whitespace()) { - Some(whitespace_count) => { - if let Some(argument_text) = parts.next() { - argument = Some(argument_text.to_string()); - end += whitespace_count + argument_text.len(); - } - } - None => { - // Rest of line is entirely whitespace - end += rest_of_line.len() - mode_text.len(); - } - } + if let Some(argument_text) = parts.next() { + end += argument_text.len(); + argument = Some(argument_text.to_string()); } Some(Self { source_range: last_mention_start + offset_to_line..end + offset_to_line, - mode, argument, }) } @@ -931,96 +321,254 @@ impl MentionCompletion { #[cfg(test)] mod tests { use super::*; + use gpui::{EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext}; + use project::{Project, ProjectPath}; + use serde_json::json; + use settings::SettingsStore; + use std::{ops::Deref, rc::Rc}; + use util::path; + use workspace::{AppState, Item}; #[test] fn test_mention_completion_parse() { - assert_eq!(MentionCompletion::try_parse(true, "Lorem Ipsum", 0), None); + assert_eq!(MentionCompletion::try_parse("Lorem Ipsum", 0), None); assert_eq!( - MentionCompletion::try_parse(true, "Lorem @", 0), + MentionCompletion::try_parse("Lorem @", 0), Some(MentionCompletion { source_range: 6..7, - mode: None, argument: None, }) ); assert_eq!( - MentionCompletion::try_parse(true, "Lorem @file", 0), + MentionCompletion::try_parse("Lorem @main", 0), Some(MentionCompletion { source_range: 6..11, - mode: Some(ContextPickerMode::File), - argument: None, - }) - ); - - assert_eq!( - MentionCompletion::try_parse(true, "Lorem @file ", 0), - Some(MentionCompletion { - source_range: 6..12, - mode: Some(ContextPickerMode::File), - argument: None, - }) - ); - - assert_eq!( - MentionCompletion::try_parse(true, "Lorem @file main.rs", 0), - Some(MentionCompletion { - source_range: 6..19, - mode: Some(ContextPickerMode::File), - argument: Some("main.rs".to_string()), - }) - ); - - assert_eq!( - MentionCompletion::try_parse(true, "Lorem @file main.rs ", 0), - Some(MentionCompletion { - source_range: 6..19, - mode: Some(ContextPickerMode::File), - argument: Some("main.rs".to_string()), - }) - ); - - assert_eq!( - MentionCompletion::try_parse(true, "Lorem @file main.rs Ipsum", 0), - Some(MentionCompletion { - source_range: 6..19, - mode: Some(ContextPickerMode::File), - argument: Some("main.rs".to_string()), - }) - ); - - assert_eq!( - MentionCompletion::try_parse(true, "Lorem @main", 0), - Some(MentionCompletion { - source_range: 6..11, - mode: None, argument: Some("main".to_string()), }) ); - assert_eq!(MentionCompletion::try_parse(true, "test@", 0), None); + assert_eq!(MentionCompletion::try_parse("test@", 0), None); + } - // Allowed non-file mentions + struct AtMentionEditor(Entity); - assert_eq!( - MentionCompletion::try_parse(true, "Lorem @symbol main", 0), - Some(MentionCompletion { - source_range: 6..18, - mode: Some(ContextPickerMode::Symbol), - argument: Some("main".to_string()), - }) - ); + impl Item for AtMentionEditor { + type Event = (); - // Disallowed non-file mentions + fn include_in_nav_history() -> bool { + false + } - assert_eq!( - MentionCompletion::try_parse(false, "Lorem @symbol main", 0), - Some(MentionCompletion { - source_range: 6..18, - mode: None, - argument: Some("main".to_string()), - }) - ); + fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { + "Test".into() + } + } + + impl EventEmitter<()> for AtMentionEditor {} + + impl Focusable for AtMentionEditor { + fn focus_handle(&self, cx: &App) -> FocusHandle { + self.0.read(cx).focus_handle(cx).clone() + } + } + + impl Render for AtMentionEditor { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + self.0.clone().into_any_element() + } + } + + #[gpui::test] + async fn test_context_completion_provider(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": "", + "two.txt": "", + "three.txt": "", + "four.txt": "" + }, + "b": { + "five.txt": "", + "six.txt": "", + "seven.txt": "", + "eight.txt": "", + } + }), + ) + .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::>(); + 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.deref(), 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 editor = workspace.update_in(&mut cx, |workspace, window, cx| { + let editor = cx.new(|cx| { + Editor::new( + editor::EditorMode::full(), + multi_buffer::MultiBuffer::build_simple("", cx), + None, + window, + cx, + ) + }); + workspace.active_pane().update(cx, |pane, cx| { + pane.add_item( + Box::new(cx.new(|_| AtMentionEditor(editor.clone()))), + true, + true, + None, + window, + cx, + ); + }); + editor + }); + + let mention_set = Arc::new(Mutex::new(MentionSet::default())); + + let editor_entity = editor.downgrade(); + editor.update_in(&mut cx, |editor, window, cx| { + window.focus(&editor.focus_handle(cx)); + editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new( + mention_set.clone(), + workspace.downgrade(), + editor_entity, + )))); + }); + + 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/", + "four.txt dir/a/", + "three.txt dir/a/", + "two.txt dir/a/", + "one.txt dir/a/", + "dir ", + "a dir/", + "four.txt dir/a/", + "one.txt dir/a/", + "three.txt dir/a/", + "two.txt dir/a/", + "b dir/", + "eight.txt dir/b/", + "five.txt dir/b/", + "seven.txt dir/b/", + "six.txt dir/b/", + "editor dir/" + ] + ); + }); + + // 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 [@four.txt](@file:dir/a/four.txt) "); + }); + } + + fn current_completion_labels(editor: &Editor) -> Vec { + let completions = editor.current_completions().expect("Missing completions"); + completions + .into_iter() + .map(|completion| completion.label.text.to_string()) + .collect::>() + } + + pub(crate) fn init_test(cx: &mut TestAppContext) { + cx.update(|cx| { + let store = SettingsStore::test(cx); + cx.set_global(store); + theme::init(theme::LoadThemes::JustBase, cx); + client::init_settings(cx); + language::init(cx); + Project::init_settings(cx); + workspace::init_settings(cx); + editor::init_settings(cx); + }); } } diff --git a/crates/agent_ui/src/acp/entry_view_state.rs b/crates/agent_ui/src/acp/entry_view_state.rs deleted file mode 100644 index becf6953fd..0000000000 --- a/crates/agent_ui/src/acp/entry_view_state.rs +++ /dev/null @@ -1,524 +0,0 @@ -use std::{cell::Cell, ops::Range, rc::Rc}; - -use acp_thread::{AcpThread, AgentThreadEntry}; -use agent_client_protocol::{PromptCapabilities, ToolCallId}; -use agent2::HistoryStore; -use collections::HashMap; -use editor::{Editor, EditorMode, MinimapVisibility}; -use gpui::{ - AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable, ScrollHandle, - 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, - project: Entity, - history_store: Entity, - prompt_store: Option>, - entries: Vec, - prevent_slash_commands: bool, - prompt_capabilities: Rc>, -} - -impl EntryViewState { - pub fn new( - workspace: WeakEntity, - project: Entity, - history_store: Entity, - prompt_store: Option>, - prompt_capabilities: Rc>, - prevent_slash_commands: bool, - ) -> Self { - Self { - workspace, - project, - history_store, - prompt_store, - entries: Vec::new(), - prevent_slash_commands, - prompt_capabilities, - } - } - - pub fn entry(&self, index: usize) -> Option<&Entry> { - self.entries.get(index) - } - - pub fn sync_entry( - &mut self, - index: usize, - thread: &Entity, - window: &mut Window, - cx: &mut Context, - ) { - 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(), - "Edit message - @ to include context", - self.prevent_slash_commands, - 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::>(); - let diffs = tool_call.diffs().cloned().collect::>(); - - 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 - }; - - for terminal in terminals { - views.entry(terminal.entity_id()).or_insert_with(|| { - 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()), - }); - element - }); - } - - 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) { - self.entries.drain(range); - } - - pub fn settings_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::() { - diff_editor.update(cx, |diff_editor, cx| { - diff_editor.set_text_style_refinement( - diff_editor_text_style_refinement(cx), - ); - cx.notify(); - }) - } - } - } - } - } - } -} - -impl EventEmitter for EntryViewState {} - -pub struct EntryViewEvent { - pub entry_index: usize, - pub view_event: ViewEvent, -} - -pub enum ViewEvent { - NewDiff(ToolCallId), - NewTerminal(ToolCallId), - MessageEditorEvent(Entity, MessageEditorEvent), -} - -#[derive(Default, Debug)] -pub struct AssistantMessageEntry { - scroll_handles_by_chunk_index: HashMap, -} - -impl AssistantMessageEntry { - pub fn scroll_handle_for_chunk(&self, ix: usize) -> Option { - 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), - AssistantMessage(AssistantMessageEntry), - Content(HashMap), -} - -impl Entry { - pub fn message_editor(&self) -> Option<&Entity> { - match self { - Self::UserMessage(editor) => Some(editor), - Self::AssistantMessage(_) | Self::Content(_) => None, - } - } - - pub fn editor_for_diff(&self, diff: &Entity) -> Option> { - self.content_map()? - .get(&diff.entity_id()) - .cloned() - .map(|entity| entity.downcast::().unwrap()) - } - - pub fn terminal( - &self, - terminal: &Entity, - ) -> Option> { - self.content_map()? - .get(&terminal.entity_id()) - .cloned() - .map(|entity| entity.downcast::().unwrap()) - } - - pub fn scroll_handle_for_assistant_message_chunk( - &self, - chunk_ix: usize, - ) -> Option { - match self { - Self::AssistantMessage(message) => message.scroll_handle_for_chunk(chunk_ix), - Self::UserMessage(_) | Self::Content(_) => None, - } - } - - fn content_map(&self) -> Option<&HashMap> { - 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, - project: Entity, - terminal: Entity, - window: &mut Window, - cx: &mut App, -) -> Entity { - 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, - window: &mut Window, - cx: &mut App, -) -> Entity { - 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(), - false, - ) - }); - - 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::>() - }); - 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); - }); - } -} diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs deleted file mode 100644 index 12ae893c31..0000000000 --- a/crates/agent_ui/src/acp/message_editor.rs +++ /dev/null @@ -1,2286 +0,0 @@ -use crate::{ - acp::completion_provider::ContextPickerCompletionProvider, - 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; -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, MultiBuffer, - SemanticsProvider, ToOffset, - actions::Paste, - display_map::{Crease, CreaseId, FoldId}, -}; -use futures::{ - FutureExt as _, - future::{Shared, join_all}, -}; -use gpui::{ - Animation, AnimationExt as _, AppContext, ClipboardEntry, Context, Entity, EntityId, - EventEmitter, FocusHandle, Focusable, HighlightStyle, Image, ImageFormat, Img, KeyContext, - Subscription, Task, TextStyle, UnderlineStyle, WeakEntity, pulsating_between, -}; -use language::{Buffer, Language}; -use language_model::LanguageModelImage; -use postage::stream::Stream as _; -use project::{CompletionIntent, Project, ProjectItem, ProjectPath, Worktree}; -use prompt_store::{PromptId, PromptStore}; -use rope::Point; -use settings::Settings; -use std::{ - cell::Cell, - ffi::OsStr, - fmt::Write, - ops::{Range, RangeInclusive}, - path::{Path, PathBuf}, - rc::Rc, - sync::Arc, - time::Duration, -}; -use text::{OffsetRangeExt, ToOffset as _}; -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, SharedString, Styled, - TextSize, TintColor, Toggleable, Window, div, h_flex, px, -}; -use util::{ResultExt, debug_panic}; -use workspace::{Workspace, notifications::NotifyResultExt as _}; -use zed_actions::agent::Chat; - -const PARSE_SLASH_COMMAND_DEBOUNCE: Duration = Duration::from_millis(50); - -pub struct MessageEditor { - mention_set: MentionSet, - editor: Entity, - project: Entity, - workspace: WeakEntity, - history_store: Entity, - prompt_store: Option>, - prevent_slash_commands: bool, - prompt_capabilities: Rc>, - _subscriptions: Vec, - _parse_slash_command_task: Task<()>, -} - -#[derive(Clone, Copy, Debug)] -pub enum MessageEditorEvent { - Send, - Cancel, - Focus, - LostFocus, -} - -impl EventEmitter for MessageEditor {} - -impl MessageEditor { - pub fn new( - workspace: WeakEntity, - project: Entity, - history_store: Entity, - prompt_store: Option>, - prompt_capabilities: Rc>, - placeholder: impl Into>, - prevent_slash_commands: bool, - mode: EditorMode, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let language = Language::new( - language::LanguageConfig { - completion_query_characters: HashSet::from_iter(['.', '-', '_', '@']), - ..Default::default() - }, - None, - ); - let completion_provider = ContextPickerCompletionProvider::new( - cx.weak_entity(), - workspace.clone(), - history_store.clone(), - prompt_store.clone(), - prompt_capabilities.clone(), - ); - let semantics_provider = Rc::new(SlashCommandSemanticsProvider { - range: Cell::new(None), - }); - 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, 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(completion_provider))); - editor.set_context_menu_options(ContextMenuOptions { - min_entries_visible: 12, - max_entries_visible: 12, - placement: Some(ContextMenuPlacement::Above), - }); - if prevent_slash_commands { - editor.set_semantics_provider(Some(semantics_provider.clone())); - } - 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 subscriptions = Vec::new(); - subscriptions.push(cx.subscribe_in(&editor, window, { - let semantics_provider = semantics_provider.clone(); - move |this, editor, event, window, cx| { - if let EditorEvent::Edited { .. } = event { - if prevent_slash_commands { - this.highlight_slash_command( - semantics_provider.clone(), - editor.clone(), - window, - cx, - ); - } - let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx)); - this.mention_set.remove_invalid(snapshot); - cx.notify(); - } - } - })); - - Self { - editor, - project, - mention_set, - workspace, - history_store, - prompt_store, - prevent_slash_commands, - prompt_capabilities, - _subscriptions: subscriptions, - _parse_slash_command_task: Task::ready(()), - } - } - - pub fn insert_thread_summary( - &mut self, - thread: agent2::DbThreadMetadata, - window: &mut Window, - cx: &mut Context, - ) { - 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_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 { - &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 { - self.mention_set - .mentions - .values() - .map(|(uri, _)| uri.clone()) - .collect() - } - - pub fn confirm_completion( - &mut self, - crease_text: SharedString, - start: text::Anchor, - content_len: usize, - mention_uri: MentionUri, - window: &mut Window, - cx: &mut Context, - ) -> 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, - ) -> Task> { - 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, - ) -> Task> { - fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<(Arc, 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 Some(worktree) = self.project.read(cx).worktree_for_entry(entry.id, cx) else { - return Task::ready(Err(anyhow!("worktree not found"))); - }; - let project = self.project.clone(); - cx.spawn(async move |_, cx| { - let directory_path = entry.path.clone(); - - let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id())?; - 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, - ) -> Task> { - 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, - cx: &mut Context, - ) -> Task> { - 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, - ) -> Task> { - 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, - selections: Vec<(Entity, Range, Range)>, - window: &mut Window, - cx: &mut Context, - ) { - 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::(); - 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, - ) -> Task> { - let server = Rc::new(agent2::NativeAgentServer::new( - self.project.read(cx).fs().clone(), - self.history_store.clone(), - )); - let connection = server.connect(Path::new(""), &self.project, cx); - cx.spawn(async move |_, cx| { - let agent = connection.await?; - let agent = agent.downcast::().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, - ) -> Task> { - 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(), - }) - }) - } - - pub fn contents( - &self, - cx: &mut Context, - ) -> Task, Vec>)>> { - let contents = self - .mention_set - .contents(&self.prompt_capabilities.get(), cx); - let editor = self.editor.clone(); - let prevent_slash_commands = self.prevent_slash_commands; - - cx.spawn(async move |_, cx| { - let contents = contents.await?; - let mut all_tracked_buffers = Vec::new(); - - editor.update(cx, |editor, cx| { - let mut ix = 0; - let mut chunks: Vec = 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 { - 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() - }; - 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() { - 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() - }; - if !last_chunk.is_empty() { - chunks.push(last_chunk.into()); - } - } - }); - - (chunks, all_tracked_buffers) - }) - }) - } - - pub fn clear(&mut self, window: &mut Window, cx: &mut Context) { - 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) { - if self.is_empty(cx) { - return; - } - cx.emit(MessageEditorEvent::Send) - } - - fn cancel(&mut self, _: &editor::actions::Cancel, _: &mut Window, cx: &mut Context) { - cx.emit(MessageEditorEvent::Cancel) - } - - fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context) { - 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::>() - }) - .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, - added_worktrees: Vec>, - window: &mut Window, - cx: &mut Context, - ) { - 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_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) { - 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.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.editor.update(cx, |editor, cx| { - editor.set_mode(mode); - cx.notify() - }); - } - - pub fn set_message( - &mut self, - message: Vec, - window: &mut Window, - cx: &mut Context, - ) { - 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(); - } - - fn highlight_slash_command( - &mut self, - semantics_provider: Rc, - editor: Entity, - window: &mut Window, - cx: &mut Context, - ) { - struct InvalidSlashCommand; - - self._parse_slash_command_task = cx.spawn_in(window, async move |_, cx| { - cx.background_executor() - .timer(PARSE_SLASH_COMMAND_DEBOUNCE) - .await; - editor - .update_in(cx, |editor, window, cx| { - let snapshot = editor.snapshot(window, cx); - let range = parse_slash_command(&editor.text(cx)); - semantics_provider.range.set(range); - if let Some((start, end)) = range { - editor.highlight_text::( - vec![ - snapshot.buffer_snapshot.anchor_after(start) - ..snapshot.buffer_snapshot.anchor_before(end), - ], - HighlightStyle { - underline: Some(UnderlineStyle { - thickness: px(1.), - color: Some(gpui::red()), - wavy: true, - }), - ..Default::default() - }, - cx, - ); - } else { - editor.clear_highlights::(cx); - } - }) - .ok(); - }) - } - - 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.editor.update(cx, |editor, cx| { - editor.set_text(text, window, cx); - }); - } -} - -fn render_directory_contents(entries: Vec<(Arc, 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) -> 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(), - ..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>, - image: Option, String>>>>, - editor: Entity, - 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_fold_icon_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_fold_icon_button( - label: SharedString, - icon: SharedString, - range: Range, - mut loading_finished: postage::barrier::Receiver, - image_task: Option, String>>>>, - editor: WeakEntity, - cx: &mut App, -) -> Arc, &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, - editor: WeakEntity, - loading: Option>, - image: Option, String>>>>, -} - -impl Render for LoadingContext { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> 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::(|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>, - _task: Task<()>, -} - -impl Render for ImageHover { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> 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>, - }, - Image(MentionImage), - UriOnly, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct MentionImage { - pub data: SharedString, - pub format: ImageFormat, -} - -#[derive(Default)] -pub struct MentionSet { - mentions: HashMap>>)>, -} - -impl MentionSet { - fn contents( - &self, - prompt_capabilities: &acp::PromptCapabilities, - cx: &mut App, - ) -> Task>> { - 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); - } - } - } -} - -struct SlashCommandSemanticsProvider { - range: Cell>, -} - -impl SemanticsProvider for SlashCommandSemanticsProvider { - fn hover( - &self, - buffer: &Entity, - position: text::Anchor, - cx: &mut App, - ) -> Option>>> { - let snapshot = buffer.read(cx).snapshot(); - let offset = position.to_offset(&snapshot); - let (start, end) = self.range.get()?; - if !(start..end).contains(&offset) { - return None; - } - let range = snapshot.anchor_after(start)..snapshot.anchor_after(end); - Some(Task::ready(Some(vec![project::Hover { - contents: vec![project::HoverBlock { - text: "Slash commands are not supported".into(), - kind: project::HoverBlockKind::PlainText, - }], - range: Some(range), - language: None, - }]))) - } - - fn inline_values( - &self, - _buffer_handle: Entity, - _range: Range, - _cx: &mut App, - ) -> Option>>> { - None - } - - fn inlay_hints( - &self, - _buffer_handle: Entity, - _range: Range, - _cx: &mut App, - ) -> Option>>> { - None - } - - fn resolve_inlay_hint( - &self, - _hint: project::InlayHint, - _buffer_handle: Entity, - _server_id: lsp::LanguageServerId, - _cx: &mut App, - ) -> Option>> { - None - } - - fn supports_inlay_hints(&self, _buffer: &Entity, _cx: &mut App) -> bool { - false - } - - fn document_highlights( - &self, - _buffer: &Entity, - _position: text::Anchor, - _cx: &mut App, - ) -> Option>>> { - None - } - - fn definitions( - &self, - _buffer: &Entity, - _position: text::Anchor, - _kind: editor::GotoDefinitionKind, - _cx: &mut App, - ) -> Option>>>> { - None - } - - fn range_for_rename( - &self, - _buffer: &Entity, - _position: text::Anchor, - _cx: &mut App, - ) -> Option>>>> { - None - } - - fn perform_rename( - &self, - _buffer: &Entity, - _position: text::Anchor, - _new_name: String, - _cx: &mut App, - ) -> Option>> { - None - } -} - -fn parse_slash_command(text: &str) -> Option<(usize, usize)> { - if let Some(remainder) = text.strip_prefix('/') { - let pos = remainder - .find(char::is_whitespace) - .unwrap_or(remainder.len()); - let command = &remainder[..pos]; - if !command.is_empty() && command.chars().all(char::is_alphanumeric) { - return Some((0, 1 + command.len())); - } - } - None -} - -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, 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(), - "Test", - false, - 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::>() - .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 { .. }]); - } - - struct MessageEditorItem(Entity); - - 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) -> impl IntoElement { - self.0.clone().into_any_element() - } - } - - #[gpui::test] - async fn test_context_completion_provider(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::>(); - 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(), - "Test", - false, - 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()); - - // Only files since we have default capabilities - 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::>(); - - { - 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::().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::>(); - - { - let [(uri, Mention::UriOnly)] = contents.as_slice() else { - panic!("Unexpected mentions"); - }; - pretty_assertions::assert_eq!(uri, &url_one.parse::().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::>(); - - 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::().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::( - move |_, _| async move { - Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![ - #[allow(deprecated)] - lsp::SymbolInformation { - name: "MySymbol".into(), - location: lsp::Location { - uri: lsp::Url::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::>(); - - { - 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::() - .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> { - 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 { - let completions = editor.current_completions().expect("Missing completions"); - completions - .into_iter() - .map(|completion| completion.label.text) - .collect::>() - } -} diff --git a/crates/agent_ui/src/acp/message_history.rs b/crates/agent_ui/src/acp/message_history.rs new file mode 100644 index 0000000000..d0fb1f0990 --- /dev/null +++ b/crates/agent_ui/src/acp/message_history.rs @@ -0,0 +1,87 @@ +pub struct MessageHistory { + items: Vec, + current: Option, +} + +impl Default for MessageHistory { + fn default() -> Self { + MessageHistory { + items: Vec::new(), + current: None, + } + } +} + +impl MessageHistory { + 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")); + } +} diff --git a/crates/agent_ui/src/acp/model_selector.rs b/crates/agent_ui/src/acp/model_selector.rs deleted file mode 100644 index 77c88c461d..0000000000 --- a/crates/agent_ui/src/acp/model_selector.rs +++ /dev/null @@ -1,472 +0,0 @@ -use std::{cmp::Reverse, rc::Rc, sync::Arc}; - -use acp_thread::{AgentModelInfo, AgentModelList, AgentModelSelector}; -use agent_client_protocol as acp; -use anyhow::Result; -use collections::IndexMap; -use futures::FutureExt; -use fuzzy::{StringMatchCandidate, match_strings}; -use gpui::{Action, AsyncWindowContext, BackgroundExecutor, DismissEvent, Task, WeakEntity}; -use ordered_float::OrderedFloat; -use picker::{Picker, PickerDelegate}; -use ui::{ - AnyElement, App, Context, IntoElement, ListItem, ListItemSpacing, SharedString, Window, - prelude::*, rems, -}; -use util::ResultExt; - -pub type AcpModelSelector = Picker; - -pub fn acp_model_selector( - session_id: acp::SessionId, - selector: Rc, - window: &mut Window, - cx: &mut Context, -) -> AcpModelSelector { - let delegate = AcpModelPickerDelegate::new(session_id, selector, window, cx); - Picker::list(delegate, window, cx) - .show_scrollbar(true) - .width(rems(20.)) - .max_height(Some(rems(20.).into())) -} - -enum AcpModelPickerEntry { - Separator(SharedString), - Model(AgentModelInfo), -} - -pub struct AcpModelPickerDelegate { - session_id: acp::SessionId, - selector: Rc, - filtered_entries: Vec, - models: Option, - selected_index: usize, - selected_model: Option, - _refresh_models_task: Task<()>, -} - -impl AcpModelPickerDelegate { - fn new( - session_id: acp::SessionId, - selector: Rc, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let mut rx = selector.watch(cx); - let refresh_models_task = cx.spawn_in(window, { - let session_id = session_id.clone(); - async move |this, cx| { - async fn refresh( - this: &WeakEntity>, - session_id: &acp::SessionId, - cx: &mut AsyncWindowContext, - ) -> Result<()> { - let (models_task, selected_model_task) = this.update(cx, |this, cx| { - ( - this.delegate.selector.list_models(cx), - this.delegate.selector.selected_model(session_id, cx), - ) - })?; - - let (models, selected_model) = futures::join!(models_task, selected_model_task); - - 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(()) - } - - refresh(&this, &session_id, cx).await.log_err(); - while let Ok(()) = rx.recv().await { - refresh(&this, &session_id, cx).await.log_err(); - } - } - }); - - Self { - session_id, - selector, - filtered_entries: Vec::new(), - models: None, - selected_model: None, - selected_index: 0, - _refresh_models_task: refresh_models_task, - } - } - - pub fn active_model(&self) -> Option<&AgentModelInfo> { - self.selected_model.as_ref() - } -} - -impl PickerDelegate for AcpModelPickerDelegate { - type ListItem = AnyElement; - - fn match_count(&self) -> usize { - self.filtered_entries.len() - } - - fn selected_index(&self) -> usize { - self.selected_index - } - - fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context>) { - self.selected_index = ix.min(self.filtered_entries.len().saturating_sub(1)); - cx.notify(); - } - - fn can_select( - &mut self, - ix: usize, - _window: &mut Window, - _cx: &mut Context>, - ) -> bool { - match self.filtered_entries.get(ix) { - Some(AcpModelPickerEntry::Model(_)) => true, - Some(AcpModelPickerEntry::Separator(_)) | None => false, - } - } - - fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - "Select a model…".into() - } - - fn update_matches( - &mut self, - query: String, - window: &mut Window, - cx: &mut Context>, - ) -> Task<()> { - cx.spawn_in(window, async move |this, cx| { - let filtered_models = match this - .read_with(cx, |this, cx| { - this.delegate.models.clone().map(move |models| { - fuzzy_search(models, query, cx.background_executor().clone()) - }) - }) - .ok() - .flatten() - { - Some(task) => task.await, - None => AgentModelList::Flat(vec![]), - }; - - this.update_in(cx, |this, window, cx| { - this.delegate.filtered_entries = - info_list_to_picker_entries(filtered_models).collect(); - // Finds the currently selected model in the list - let new_index = this - .delegate - .selected_model - .as_ref() - .and_then(|selected| { - this.delegate.filtered_entries.iter().position(|entry| { - if let AcpModelPickerEntry::Model(model_info) = entry { - model_info.id == selected.id - } else { - false - } - }) - }) - .unwrap_or(0); - this.set_selected_index(new_index, Some(picker::Direction::Down), true, window, cx); - cx.notify(); - }) - .ok(); - }) - } - - fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context>) { - if let Some(AcpModelPickerEntry::Model(model_info)) = - self.filtered_entries.get(self.selected_index) - { - self.selector - .select_model(self.session_id.clone(), model_info.id.clone(), cx) - .detach_and_log_err(cx); - self.selected_model = Some(model_info.clone()); - let current_index = self.selected_index; - self.set_selected_index(current_index, window, cx); - - cx.emit(DismissEvent); - } - } - - fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { - cx.emit(DismissEvent); - } - - fn render_match( - &self, - ix: usize, - selected: bool, - _: &mut Window, - cx: &mut Context>, - ) -> Option { - match self.filtered_entries.get(ix)? { - AcpModelPickerEntry::Separator(title) => Some( - div() - .px_2() - .pb_1() - .when(ix > 1, |this| { - this.mt_1() - .pt_2() - .border_t_1() - .border_color(cx.theme().colors().border_variant) - }) - .child( - Label::new(title) - .size(LabelSize::XSmall) - .color(Color::Muted), - ) - .into_any_element(), - ), - AcpModelPickerEntry::Model(model_info) => { - let is_selected = Some(model_info) == self.selected_model.as_ref(); - - let model_icon_color = if is_selected { - Color::Accent - } else { - Color::Muted - }; - - Some( - ListItem::new(ix) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .toggle_state(selected) - .start_slot::(model_info.icon.map(|icon| { - Icon::new(icon) - .color(model_icon_color) - .size(IconSize::Small) - })) - .child( - h_flex() - .w_full() - .pl_0p5() - .gap_1p5() - .w(px(240.)) - .child(Label::new(model_info.name.clone()).truncate()), - ) - .end_slot(div().pr_3().when(is_selected, |this| { - this.child( - Icon::new(IconName::Check) - .color(Color::Accent) - .size(IconSize::Small), - ) - })) - .into_any_element(), - ) - } - } - } - - fn render_footer( - &self, - _: &mut Window, - cx: &mut Context>, - ) -> Option { - Some( - h_flex() - .w_full() - .border_t_1() - .border_color(cx.theme().colors().border_variant) - .p_1() - .gap_4() - .justify_between() - .child( - Button::new("configure", "Configure") - .icon(IconName::Settings) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .icon_position(IconPosition::Start) - .on_click(|_, window, cx| { - window.dispatch_action( - zed_actions::agent::OpenSettings.boxed_clone(), - cx, - ); - }), - ) - .into_any(), - ) - } -} - -fn info_list_to_picker_entries( - model_list: AgentModelList, -) -> impl Iterator { - match model_list { - AgentModelList::Flat(list) => { - itertools::Either::Left(list.into_iter().map(AcpModelPickerEntry::Model)) - } - AgentModelList::Grouped(index_map) => { - itertools::Either::Right(index_map.into_iter().flat_map(|(group_name, models)| { - std::iter::once(AcpModelPickerEntry::Separator(group_name.0)) - .chain(models.into_iter().map(AcpModelPickerEntry::Model)) - })) - } - } -} - -async fn fuzzy_search( - model_list: AgentModelList, - query: String, - executor: BackgroundExecutor, -) -> AgentModelList { - async fn fuzzy_search_list( - model_list: Vec, - query: &str, - executor: BackgroundExecutor, - ) -> Vec { - let candidates = model_list - .iter() - .enumerate() - .map(|(ix, model)| { - StringMatchCandidate::new(ix, &format!("{}/{}", model.id, model.name)) - }) - .collect::>(); - let mut matches = match_strings( - &candidates, - query, - false, - true, - 100, - &Default::default(), - executor, - ) - .await; - - matches.sort_unstable_by_key(|mat| { - let candidate = &candidates[mat.candidate_id]; - (Reverse(OrderedFloat(mat.score)), candidate.id) - }); - - matches - .into_iter() - .map(|mat| model_list[mat.candidate_id].clone()) - .collect() - } - - match model_list { - AgentModelList::Flat(model_list) => { - AgentModelList::Flat(fuzzy_search_list(model_list, &query, executor).await) - } - AgentModelList::Grouped(index_map) => { - let groups = - futures::future::join_all(index_map.into_iter().map(|(group_name, models)| { - fuzzy_search_list(models, &query, executor.clone()) - .map(|results| (group_name, results)) - })) - .await; - AgentModelList::Grouped(IndexMap::from_iter( - groups - .into_iter() - .filter(|(_, results)| !results.is_empty()), - )) - } - } -} - -#[cfg(test)] -mod tests { - use gpui::TestAppContext; - - use super::*; - - fn create_model_list(grouped_models: Vec<(&str, Vec<&str>)>) -> AgentModelList { - AgentModelList::Grouped(IndexMap::from_iter(grouped_models.into_iter().map( - |(group, models)| { - ( - acp_thread::AgentModelGroupName(group.to_string().into()), - models - .into_iter() - .map(|model| acp_thread::AgentModelInfo { - id: acp_thread::AgentModelId(model.to_string().into()), - name: model.to_string().into(), - icon: None, - }) - .collect::>(), - ) - }, - ))) - } - - fn assert_models_eq(result: AgentModelList, expected: Vec<(&str, Vec<&str>)>) { - let AgentModelList::Grouped(groups) = result else { - panic!("Expected LanguageModelInfoList::Grouped, got {:?}", result); - }; - - assert_eq!( - groups.len(), - expected.len(), - "Number of groups doesn't match" - ); - - for (i, (expected_group, expected_models)) in expected.iter().enumerate() { - let (actual_group, actual_models) = groups.get_index(i).unwrap(); - assert_eq!( - actual_group.0.as_ref(), - *expected_group, - "Group at position {} doesn't match expected group", - i - ); - assert_eq!( - actual_models.len(), - expected_models.len(), - "Number of models in group {} doesn't match", - expected_group - ); - - for (j, expected_model_name) in expected_models.iter().enumerate() { - assert_eq!( - actual_models[j].name, *expected_model_name, - "Model at position {} in group {} doesn't match expected model", - j, expected_group - ); - } - } - } - - #[gpui::test] - async fn test_fuzzy_match(cx: &mut TestAppContext) { - let models = create_model_list(vec![ - ( - "zed", - vec![ - "Claude 3.7 Sonnet", - "Claude 3.7 Sonnet Thinking", - "gpt-4.1", - "gpt-4.1-nano", - ], - ), - ("openai", vec!["gpt-3.5-turbo", "gpt-4.1", "gpt-4.1-nano"]), - ("ollama", vec!["mistral", "deepseek"]), - ]); - - // Results should preserve models order whenever possible. - // In the case below, `zed/gpt-4.1` and `openai/gpt-4.1` have identical - // similarity scores, but `zed/gpt-4.1` was higher in the models list, - // so it should appear first in the results. - let results = fuzzy_search(models.clone(), "41".into(), cx.executor()).await; - assert_models_eq( - results, - vec![ - ("zed", vec!["gpt-4.1", "gpt-4.1-nano"]), - ("openai", vec!["gpt-4.1", "gpt-4.1-nano"]), - ], - ); - - // Fuzzy search - let results = fuzzy_search(models.clone(), "4n".into(), cx.executor()).await; - assert_models_eq( - results, - vec![ - ("zed", vec!["gpt-4.1-nano"]), - ("openai", vec!["gpt-4.1-nano"]), - ], - ); - } -} diff --git a/crates/agent_ui/src/acp/model_selector_popover.rs b/crates/agent_ui/src/acp/model_selector_popover.rs deleted file mode 100644 index e52101113a..0000000000 --- a/crates/agent_ui/src/acp/model_selector_popover.rs +++ /dev/null @@ -1,85 +0,0 @@ -use std::rc::Rc; - -use acp_thread::AgentModelSelector; -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::*, -}; -use zed_actions::agent::ToggleModelSelector; - -use crate::acp::{AcpModelSelector, model_selector::acp_model_selector}; - -pub struct AcpModelSelectorPopover { - selector: Entity, - menu_handle: PopoverMenuHandle, - focus_handle: FocusHandle, -} - -impl AcpModelSelectorPopover { - pub(crate) fn new( - session_id: acp::SessionId, - selector: Rc, - menu_handle: PopoverMenuHandle, - focus_handle: FocusHandle, - window: &mut Window, - cx: &mut Context, - ) -> Self { - Self { - selector: cx.new(move |cx| acp_model_selector(session_id, selector, window, cx)), - menu_handle, - focus_handle, - } - } - - pub fn toggle(&self, window: &mut Window, cx: &mut Context) { - self.menu_handle.toggle(window, cx); - } -} - -impl Render for AcpModelSelectorPopover { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let model = self.selector.read(cx).delegate.active_model(); - let model_name = model - .as_ref() - .map(|model| model.name.clone()) - .unwrap_or_else(|| SharedString::from("Select a Model")); - - let model_icon = model.as_ref().and_then(|model| model.icon); - - let focus_handle = self.focus_handle.clone(); - - 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)) - }) - .child( - Label::new(model_name) - .color(Color::Muted) - .size(LabelSize::Small) - .ml_0p5(), - ) - .child( - Icon::new(IconName::ChevronDown) - .color(Color::Muted) - .size(IconSize::XSmall), - ), - move |window, cx| { - Tooltip::for_action_in( - "Change Model", - &ToggleModelSelector, - &focus_handle, - window, - cx, - ) - }, - gpui::Corner::BottomRight, - cx, - ) - .with_handle(self.menu_handle.clone()) - .render(window, cx) - } -} diff --git a/crates/agent_ui/src/acp/thread_history.rs b/crates/agent_ui/src/acp/thread_history.rs deleted file mode 100644 index a49dae25b3..0000000000 --- a/crates/agent_ui/src/acp/thread_history.rs +++ /dev/null @@ -1,825 +0,0 @@ -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, - scroll_handle: UniformListScrollHandle, - selected_index: usize, - hovered_index: Option, - search_editor: Entity, - search_query: SharedString, - - visible_items: Vec, - - scrollbar_visibility: bool, - scrollbar_state: ScrollbarState, - local_timezone: UtcOffset, - - _update_task: Task<()>, - _subscriptions: Vec, -} - -enum ListItemType { - BucketSeparator(TimeBucket), - Entry { - entry: HistoryEntry, - format: EntryTimeFormat, - }, - SearchResult { - entry: HistoryEntry, - positions: Vec, - }, -} - -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 for AcpThreadHistory {} - -impl AcpThreadHistory { - pub(crate) fn new( - history_store: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let search_editor = cx.new(|cx| { - let mut editor = Editor::single_line(window, cx); - editor.set_placeholder_text("Search threads...", 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) { - 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, cx: &App) -> Task> { - 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, - cx: &App, - ) -> Task> { - 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) { - 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, - ) { - 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, - ) { - 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.set_selected_index(0, Bias::Right, cx); - } - - fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context) { - 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.confirm_entry(self.selected_index, cx); - } - - fn confirm_entry(&mut self, ix: usize, cx: &mut Context) { - 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.remove_thread(self.selected_index, cx) - } - - fn remove_thread(&mut self, visible_item_ix: usize, cx: &mut Context) { - 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) -> Option> { - 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, - _window: &mut Window, - cx: &mut Context, - ) -> Vec { - 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) -> 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, - cx: &Context, - ) -> 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::(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) -> 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, 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, - selected: bool, - hovered: bool, - on_hover: Box, -} - -impl AcpHistoryEntryElement { - pub fn new(entry: HistoryEntry, thread_view: WeakEntity) -> 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::(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::(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::(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 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); - } -} diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index c68c3a3e93..e46e1ae3ab 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1,286 +1,74 @@ -use acp_thread::{ - AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk, - AuthRequired, LoadError, MentionUri, RetryStatus, ThreadStatus, ToolCall, ToolCallContent, - ToolCallStatus, UserMessageId, -}; use acp_thread::{AgentConnection, Plan}; -use action_log::ActionLog; -use agent_client_protocol::{self as acp, PromptCapabilities}; -use agent_servers::{AgentServer, ClaudeCode}; -use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting}; -use agent2::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore}; -use anyhow::bail; -use audio::{Audio, Sound}; -use buffer_diff::BufferDiff; -use client::zed_urls; -use collections::{HashMap, HashSet}; -use editor::scroll::Autoscroll; -use editor::{Editor, EditorEvent, EditorMode, MultiBuffer, PathKey, SelectionEffects}; -use file_icons::FileIcons; -use fs::Fs; -use gpui::{ - 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, Transformation, UnderlineStyle, WeakEntity, - Window, WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage, - point, prelude::*, pulsating_between, -}; -use language::Buffer; - -use language_model::LanguageModelRegistry; -use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle}; -use project::{Project, ProjectEntryId}; -use prompt_store::{PromptId, PromptStore}; -use rope::Point; -use settings::{Settings as _, SettingsStore}; -use std::cell::Cell; +use agent_servers::AgentServer; +use std::cell::RefCell; +use std::collections::BTreeMap; use std::path::Path; +use std::rc::Rc; use std::sync::Arc; -use std::time::Instant; -use std::{collections::BTreeMap, rc::Rc, time::Duration}; +use std::time::Duration; + +use agent_client_protocol as acp; +use assistant_tool::ActionLog; +use buffer_diff::BufferDiff; +use collections::{HashMap, HashSet}; +use editor::{ + AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode, + EditorStyle, MinimapVisibility, MultiBuffer, PathKey, +}; +use file_icons::FileIcons; +use gpui::{ + Action, Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId, + FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, SharedString, StyleRefinement, + Subscription, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, + Window, div, linear_color_stop, linear_gradient, list, percentage, point, prelude::*, + pulsating_between, +}; +use language::language_settings::SoftWrap; +use language::{Buffer, Language}; +use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle}; +use parking_lot::Mutex; +use project::Project; +use settings::Settings as _; use text::Anchor; use theme::ThemeSettings; -use ui::{ - Callout, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding, PopoverMenuHandle, - Scrollbar, ScrollbarState, SpinnerLabel, TintColor, Tooltip, prelude::*, -}; -use util::{ResultExt, size::format_file_size, time::duration_alt_display}; +use ui::{Disclosure, Divider, DividerColor, KeyBinding, Tooltip, prelude::*}; +use util::ResultExt; use workspace::{CollaboratorId, Workspace}; -use zed_actions::agent::{Chat, ToggleModelSelector}; -use zed_actions::assistant::OpenRulesLibrary; +use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage}; -use super::entry_view_state::EntryViewState; -use crate::acp::AcpModelSelectorPopover; -use crate::acp::entry_view_state::{EntryViewEvent, ViewEvent}; -use crate::acp::message_editor::{MessageEditor, MessageEditorEvent}; +use ::acp_thread::{ + AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk, Diff, + LoadError, MentionPath, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus, +}; + +use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSet}; +use crate::acp::message_history::MessageHistory; use crate::agent_diff::AgentDiff; -use crate::profile_selector::{ProfileProvider, ProfileSelector}; +use crate::message_editor::{MAX_EDITOR_LINES, MIN_EDITOR_LINES}; +use crate::{AgentDiffPane, ExpandMessageEditor, Follow, KeepAll, OpenAgentDiff, RejectAll}; -use crate::ui::preview::UsageCallout; -use crate::ui::{ - AgentNotification, AgentNotificationEvent, BurnModeTooltip, UnavailableEditingTooltip, -}; -use crate::{ - AgentDiffPane, AgentPanel, ContinueThread, ContinueWithBurnMode, ExpandMessageEditor, Follow, - KeepAll, OpenAgentDiff, OpenHistory, RejectAll, ToggleBurnMode, ToggleProfileSelector, -}; - -pub const MIN_EDITOR_LINES: usize = 4; -pub const MAX_EDITOR_LINES: usize = 8; - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -enum ThreadFeedback { - Positive, - Negative, -} - -enum ThreadError { - PaymentRequired, - ModelRequestLimitReached(cloud_llm_client::Plan), - ToolUseLimitReached, - AuthenticationRequired(SharedString), - Other(SharedString), -} - -impl ThreadError { - fn from_err(error: anyhow::Error, agent: &Rc) -> Self { - if error.is::() { - Self::PaymentRequired - } else if error.is::() { - Self::ToolUseLimitReached - } else if let Some(error) = - error.downcast_ref::() - { - Self::ModelRequestLimitReached(error.plan) - } else { - let string = error.to_string(); - // TODO: we should have Gemini return better errors here. - if agent.clone().downcast::().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 { - 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, - comments_editor: Option>, -} - -impl ThreadFeedbackState { - pub fn submit( - &mut self, - thread: Entity, - 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, 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 { - 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.", - cx, - ); - editor - }); - - editor.read(cx).focus_handle(cx).focus(window); - editor - } -} +const RESPONSE_PADDING_X: Pixels = px(19.); pub struct AcpThreadView { agent: Rc, workspace: WeakEntity, project: Entity, thread_state: ThreadState, - history_store: Entity, - hovered_recent_history_item: Option, - entry_view_state: Entity, - message_editor: Entity, - focus_handle: FocusHandle, - model_selector: Option>, - profile_selector: Option>, - notifications: Vec>, - notification_subscriptions: HashMap, Vec>, - thread_retry_status: Option, - thread_error: Option, - thread_feedback: ThreadFeedbackState, + diff_editors: HashMap>, + message_editor: Entity, + message_set_from_history: bool, + _message_editor_subscription: Subscription, + mention_set: Arc>, + last_error: Option>, list_state: ListState, - scrollbar_state: ScrollbarState, auth_task: Option>, expanded_tool_calls: HashSet, expanded_thinking_blocks: HashSet<(usize, usize)>, edits_expanded: bool, plan_expanded: bool, editor_expanded: bool, - should_be_following: bool, - editing_message: Option, - prompt_capabilities: Rc>, - is_loading_contents: bool, - install_command_markdown: Entity, + message_history: Rc>>>, _cancel_task: Option>, - _subscriptions: [Subscription; 3], } enum ThreadState { @@ -289,119 +77,119 @@ enum ThreadState { }, Ready { thread: Entity, - title_editor: Option>, - _subscriptions: Vec, + _subscription: [Subscription; 2], }, LoadError(LoadError), Unauthenticated { connection: Rc, - description: Option>, - configuration_view: Option, - pending_auth_method: Option, - _subscription: Option, }, } impl AcpThreadView { pub fn new( agent: Rc, - resume_thread: Option, - summarize_thread: Option, workspace: WeakEntity, project: Entity, - history_store: Entity, - prompt_store: Option>, + message_history: Rc>>>, + min_lines: usize, + max_lines: Option, window: &mut Window, cx: &mut Context, ) -> Self { - let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default())); - let prevent_slash_commands = agent.clone().downcast::().is_some(); + let language = Language::new( + language::LanguageConfig { + completion_query_characters: HashSet::from_iter(['.', '-', '_', '@']), + ..Default::default() + }, + None, + ); - let placeholder = if agent.name() == "Zed Agent" { - format!("Message the {} — @ to include context", agent.name()) - } else { - format!("Message {} — @ to include context", agent.name()) - }; + let mention_set = Arc::new(Mutex::new(MentionSet::default())); let message_editor = cx.new(|cx| { - let mut editor = MessageEditor::new( - workspace.clone(), - project.clone(), - history_store.clone(), - prompt_store.clone(), - prompt_capabilities.clone(), - placeholder, - prevent_slash_commands, + 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( editor::EditorMode::AutoHeight { - min_lines: MIN_EDITOR_LINES, - max_lines: Some(MAX_EDITOR_LINES), + min_lines, + max_lines: max_lines, }, + buffer, + None, window, cx, ); - if let Some(entry) = summarize_thread { - editor.insert_thread_summary(entry, 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(), + cx.weak_entity(), + )))); + editor.set_context_menu_options(ContextMenuOptions { + min_entries_visible: 12, + max_entries_visible: 12, + placement: Some(ContextMenuPlacement::Above), + }); editor }); - let list_state = ListState::new(0, gpui::ListAlignment::Bottom, px(2048.0)); - - let entry_view_state = cx.new(|_| { - EntryViewState::new( - workspace.clone(), - project.clone(), - history_store.clone(), - prompt_store.clone(), - prompt_capabilities.clone(), - prevent_slash_commands, - ) + let message_editor_subscription = cx.subscribe(&message_editor, |this, _, event, _| { + if let editor::EditorEvent::BufferEdited = &event { + if !this.message_set_from_history { + this.message_history.borrow_mut().reset_position(); + } + this.message_set_from_history = false; + } }); - let subscriptions = [ - cx.observe_global_in::(window, Self::settings_changed), - cx.subscribe_in(&message_editor, window, Self::handle_message_editor_event), - cx.subscribe_in(&entry_view_state, window, Self::handle_entry_view_event), - ]; + let mention_set = mention_set.clone(); + + let list_state = ListState::new( + 0, + gpui::ListAlignment::Bottom, + px(2048.0), + cx.processor({ + move |this: &mut Self, index: usize, window, cx| { + let Some((entry, len)) = this.thread().and_then(|thread| { + let entries = &thread.read(cx).entries(); + Some((entries.get(index)?, entries.len())) + }) else { + return Empty.into_any(); + }; + this.render_entry(index, len, entry, window, cx) + } + }), + ); Self { agent: agent.clone(), workspace: workspace.clone(), project: project.clone(), - entry_view_state, - thread_state: Self::initial_state(agent, resume_thread, workspace, project, window, cx), + thread_state: Self::initial_state(agent, workspace, project, window, cx), message_editor, - model_selector: None, - profile_selector: None, - notifications: Vec::new(), - notification_subscriptions: HashMap::default(), - list_state: list_state.clone(), - scrollbar_state: ScrollbarState::new(list_state).parent_entity(&cx.entity()), - thread_retry_status: None, - thread_error: None, - thread_feedback: Default::default(), + message_set_from_history: false, + _message_editor_subscription: message_editor_subscription, + mention_set, + diff_editors: Default::default(), + list_state: list_state, + last_error: None, auth_task: None, expanded_tool_calls: HashSet::default(), expanded_thinking_blocks: HashSet::default(), - editing_message: None, edits_expanded: false, plan_expanded: false, editor_expanded: false, - should_be_following: false, - history_store, - hovered_recent_history_item: None, - prompt_capabilities, - is_loading_contents: false, - install_command_markdown: cx.new(|cx| Markdown::new("".into(), None, None, cx)), - _subscriptions: subscriptions, + message_history, _cancel_task: None, - focus_handle: cx.focus_handle(), } } fn initial_state( agent: Rc, - resume_thread: Option, workspace: WeakEntity, project: Entity, window: &mut Window, @@ -417,14 +205,10 @@ impl AcpThreadView { let connect_task = agent.connect(&root_dir, &project, cx); let load_task = cx.spawn_in(window, async move |this, cx| { let connection = match connect_task.await { - Ok(connection) => connection, + Ok(thread) => thread, Err(err) => { - this.update_in(cx, |this, window, cx| { - if err.downcast_ref::().is_some() { - this.handle_load_error(err, window, cx); - } else { - this.handle_thread_error(err, cx); - } + this.update(cx, |this, cx| { + this.handle_load_error(err, cx); cx.notify(); }) .log_err(); @@ -432,132 +216,51 @@ impl AcpThreadView { } }; - let result = if let Some(native_agent) = connection + let result = match connection .clone() - .downcast::() - && let Some(resume) = resume_thread.clone() + .new_thread(project.clone(), &root_dir, cx) + .await { - cx.update(|_, cx| { - native_agent - .0 - .update(cx, |agent, cx| agent.open_thread(resume.id, cx)) - }) - .log_err() - } else { - 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::() { - Ok(err) => { - cx.update(|window, cx| { - Self::handle_auth_required(this, err, agent, connection, window, cx) + Err(e) => { + let mut cx = cx.clone(); + if e.downcast_ref::().is_some() { + this.update(&mut cx, |this, cx| { + this.thread_state = ThreadState::Unauthenticated { connection }; + cx.notify(); }) - .log_err(); + .ok(); return; + } else { + Err(e) } - Err(err) => Err(err), - }, - Ok(thread) => Ok(thread), + } + Ok(session_id) => Ok(session_id), }; 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.prompt_capabilities - .set(thread.read(cx).prompt_capabilities()); - - let count = thread.read(cx).entries().len(); - this.list_state.splice(0..0, count); - this.entry_view_state.update(cx, |view_state, cx| { - for ix in 0..count { - view_state.sync_entry(ix, &thread, window, cx); - } - }); - - if let Some(resume) = resume_thread { - this.history_store.update(cx, |history, cx| { - history.push_recently_opened_entry( - HistoryEntryId::AcpThread(resume.id), - cx, - ); - }); - } + this.list_state + .splice(0..0, thread.read(cx).entries().len()); AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx); - this.model_selector = - thread - .read(cx) - .connection() - .model_selector() - .map(|selector| { - cx.new(|cx| { - AcpModelSelectorPopover::new( - thread.read(cx).session_id().clone(), - selector, - PopoverMenuHandle::default(), - this.focus_handle(cx), - window, - cx, - ) - }) - }); - - 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, - title_editor, - _subscriptions: subscriptions, + _subscription: [thread_subscription, action_log_subscription], }; - this.message_editor.focus_handle(cx).focus(window); - - this.profile_selector = this.as_native_thread(cx).map(|thread| { - cx.new(|cx| { - ProfileSelector::new( - ::global(cx), - Arc::new(thread.clone()), - this.focus_handle(cx), - cx, - ) - }) - }); cx.notify(); } Err(err) => { - this.handle_load_error(err, window, cx); + this.handle_load_error(err, cx); } }; }) @@ -567,127 +270,35 @@ impl AcpThreadView { ThreadState::Loading { _task: load_task } } - fn handle_auth_required( - this: WeakEntity, - err: AuthRequired, - agent: Rc, - connection: Rc, - 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 - { - this.update(cx, |this, cx| { - this.thread_state = Self::initial_state( - agent.clone(), - None, - this.workspace.clone(), - this.project.clone(), - window, - cx, - ); - cx.notify(); - }) - .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, - window: &mut Window, - cx: &mut Context, - ) { + fn handle_load_error(&mut self, err: anyhow::Error, cx: &mut Context) { if let Some(load_err) = err.downcast_ref::() { 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 { - &self.workspace - } - pub fn thread(&self) -> Option<&Entity> { match &self.thread_state { ThreadState::Ready { thread, .. } => Some(thread), ThreadState::Unauthenticated { .. } | ThreadState::Loading { .. } - | ThreadState::LoadError { .. } => None, + | ThreadState::LoadError(..) => None, } } - pub fn title(&self) -> SharedString { + pub fn title(&self, cx: &App) -> SharedString { match &self.thread_state { - ThreadState::Ready { .. } | ThreadState::Unauthenticated { .. } => "New Thread".into(), + ThreadState::Ready { thread, .. } => thread.read(cx).title(), ThreadState::Loading { .. } => "Loading…".into(), - ThreadState::LoadError(error) => match error { - LoadError::NotInstalled { .. } => format!("Install {}", self.agent.name()).into(), - LoadError::Unsupported { .. } => format!("Upgrade {}", self.agent.name()).into(), - LoadError::Exited { .. } => format!("{} Exited", self.agent.name()).into(), - LoadError::Other(_) => format!("Error Loading {}", self.agent.name()).into(), - }, + ThreadState::LoadError(_) => "Failed to load".into(), + ThreadState::Unauthenticated { .. } => "Not authenticated".into(), } } - pub fn title_editor(&self) -> Option> { - if let ThreadState::Ready { title_editor, .. } = &self.thread_state { - title_editor.clone() - } else { - None - } - } - - pub fn cancel_generation(&mut self, cx: &mut Context) { - self.thread_error.take(); - self.thread_retry_status.take(); + pub fn cancel(&mut self, cx: &mut Context) { + self.last_error.take(); if let Some(thread) = self.thread() { self._cancel_task = Some(thread.update(cx, |thread, cx| thread.cancel(cx))); @@ -706,332 +317,126 @@ impl AcpThreadView { fn set_editor_is_expanded(&mut self, is_expanded: bool, cx: &mut Context) { self.editor_expanded = is_expanded; - self.message_editor.update(cx, |editor, cx| { - if is_expanded { - editor.set_mode( - EditorMode::Full { - scale_ui_elements_with_buffer_font_size: false, - show_active_line_background: false, - sized_by_content: false, - }, - cx, - ) + self.message_editor.update(cx, |editor, _| { + if self.editor_expanded { + editor.set_mode(EditorMode::Full { + scale_ui_elements_with_buffer_font_size: false, + show_active_line_background: false, + sized_by_content: false, + }) } else { - editor.set_mode( - EditorMode::AutoHeight { - min_lines: MIN_EDITOR_LINES, - max_lines: Some(MAX_EDITOR_LINES), - }, - cx, - ) + editor.set_mode(EditorMode::AutoHeight { + min_lines: MIN_EDITOR_LINES, + max_lines: Some(MAX_EDITOR_LINES), + }) } }); cx.notify(); } - pub fn handle_title_editor_event( - &mut self, - title_editor: &Entity, - event: &EditorEvent, - window: &mut Window, - cx: &mut Context, - ) { - let Some(thread) = self.thread() else { return }; + fn chat(&mut self, _: &Chat, window: &mut Window, cx: &mut Context) { + self.last_error.take(); - match event { - EditorEvent::BufferEdited => { - let new_title = title_editor.read(cx).text(cx); - thread.update(cx, |thread, cx| { - thread - .set_title(new_title.into(), cx) - .detach_and_log_err(cx); - }) - } - EditorEvent::Blurred => { - if title_editor.read(cx).text(cx).is_empty() { - title_editor.update(cx, |editor, cx| { - editor.set_text("New Thread", window, cx); - }); - } - } - _ => {} - } - } - - pub fn handle_message_editor_event( - &mut self, - _: &Entity, - event: &MessageEditorEvent, - window: &mut Window, - cx: &mut Context, - ) { - match event { - MessageEditorEvent::Send => self.send(window, cx), - MessageEditorEvent::Cancel => self.cancel_generation(cx), - MessageEditorEvent::Focus => { - self.cancel_editing(&Default::default(), window, cx); - } - MessageEditorEvent::LostFocus => {} - } - } - - pub fn handle_entry_view_event( - &mut self, - _: &Entity, - event: &EntryViewEvent, - window: &mut Window, - cx: &mut Context, - ) { - match &event.view_event { - ViewEvent::NewDiff(tool_call_id) => { - if AgentSettings::get_global(cx).expand_edit_card { - self.expanded_tool_calls.insert(tool_call_id.clone()); - } - } - ViewEvent::NewTerminal(tool_call_id) => { - if AgentSettings::get_global(cx).expand_terminal_card { - self.expanded_tool_calls.insert(tool_call_id.clone()); - } - } - ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Focus) => { - if let Some(thread) = self.thread() - && let Some(AgentThreadEntry::UserMessage(user_message)) = - thread.read(cx).entries().get(event.entry_index) - && user_message.id.is_some() - { - self.editing_message = Some(event.entry_index); - cx.notify(); - } - } - ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::LostFocus) => { - if let Some(thread) = self.thread() - && let Some(AgentThreadEntry::UserMessage(user_message)) = - thread.read(cx).entries().get(event.entry_index) - && user_message.id.is_some() - { - if editor.read(cx).text(cx).as_str() == user_message.content.to_markdown(cx) { - self.editing_message = None; - cx.notify(); + let mut ix = 0; + let mut chunks: Vec = Vec::new(); + let project = self.project.clone(); + self.message_editor.update(cx, |editor, cx| { + 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() { + if let Some(project_path) = + self.mention_set.lock().path_for_crease_id(crease_id) + { + let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot); + if crease_range.start > ix { + chunks.push(text[ix..crease_range.start].into()); + } + if let Some(abs_path) = project.read(cx).absolute_path(&project_path, cx) { + let path_str = abs_path.display().to_string(); + chunks.push(acp::ContentBlock::ResourceLink(acp::ResourceLink { + uri: path_str.clone(), + name: path_str, + annotations: None, + description: None, + mime_type: None, + size: None, + title: None, + })); + } + ix = crease_range.end; } } - } - ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::Send) => { - self.regenerate(event.entry_index, editor, window, cx); - } - ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Cancel) => { - self.cancel_editing(&Default::default(), window, cx); - } - } - } - fn resume_chat(&mut self, cx: &mut Context) { - self.thread_error.take(); - let Some(thread) = self.thread() else { - return; - }; - if !thread.read(cx).can_resume(cx) { + if ix < text.len() { + let last_chunk = text[ix..].trim(); + if !last_chunk.is_empty() { + chunks.push(last_chunk.into()); + } + } + }) + }); + + if chunks.is_empty() { return; } - let task = thread.update(cx, |thread, cx| thread.resume(cx)); + let Some(thread) = self.thread() else { return }; + let task = thread.update(cx, |thread, cx| thread.send(chunks.clone(), cx)); + cx.spawn(async move |this, cx| { let result = task.await; this.update(cx, |this, cx| { if let Err(err) = result { - this.handle_thread_error(err, cx); + this.last_error = + Some(cx.new(|cx| Markdown::new(err.to_string().into(), None, None, cx))) } }) }) .detach(); - } - fn send(&mut self, window: &mut Window, cx: &mut Context) { - let Some(thread) = self.thread() else { return }; + let mention_set = self.mention_set.clone(); - if self.is_loading_contents { - return; - } - - self.history_store.update(cx, |history, cx| { - history.push_recently_opened_entry( - HistoryEntryId::AcpThread(thread.read(cx).session_id().clone()), - cx, - ); + self.set_editor_is_expanded(false, cx); + self.message_editor.update(cx, |editor, cx| { + editor.clear(window, cx); + editor.remove_creases(mention_set.lock().drain(), cx) }); - if thread.read(cx).status() != ThreadStatus::Idle { - self.stop_current_and_send_new_message(window, cx); - return; - } - - let contents = self - .message_editor - .update(cx, |message_editor, cx| message_editor.contents(cx)); - self.send_impl(contents, window, cx) + self.message_history.borrow_mut().push(chunks); } - fn stop_current_and_send_new_message(&mut self, window: &mut Window, cx: &mut Context) { - let Some(thread) = self.thread().cloned() else { - return; - }; - - let cancelled = thread.update(cx, |thread, cx| thread.cancel(cx)); - - let contents = self - .message_editor - .update(cx, |message_editor, cx| message_editor.contents(cx)); - - cx.spawn_in(window, async move |this, cx| { - cancelled.await; - - this.update_in(cx, |this, window, cx| { - this.send_impl(contents, window, cx); - }) - .ok(); - }) - .detach(); - } - - fn send_impl( + fn previous_history_message( &mut self, - contents: Task, Vec>)>>, + _: &PreviousHistoryMessage, window: &mut Window, cx: &mut Context, ) { - let agent_telemetry_id = self.agent.telemetry_id(); - - self.thread_error.take(); - self.editing_message.take(); - self.thread_feedback.clear(); - - let Some(thread) = self.thread().cloned() else { - return; - }; - if self.should_be_following { - self.workspace - .update(cx, |workspace, cx| { - workspace.follow(CollaboratorId::Agent, window, cx); - }) - .ok(); - } - - self.is_loading_contents = true; - let guard = cx.new(|_| ()); - cx.observe_release(&guard, |this, _guard, cx| { - this.is_loading_contents = false; - cx.notify(); - }) - .detach(); - - let task = cx.spawn_in(window, async move |this, cx| { - let (contents, tracked_buffers) = contents.await?; - - if contents.is_empty() { - return Ok(()); - } - - this.update_in(cx, |this, window, cx| { - this.set_editor_is_expanded(false, cx); - this.scroll_to_bottom(cx); - this.message_editor.update(cx, |message_editor, cx| { - message_editor.clear(window, cx); - }); - })?; - let send = thread.update(cx, |thread, cx| { - thread.action_log().update(cx, |action_log, cx| { - for buffer in tracked_buffers { - action_log.buffer_read(buffer, cx) - } - }); - drop(guard); - - telemetry::event!("Agent Message Sent", agent = agent_telemetry_id); - - thread.send(contents, cx) - })?; - send.await - }); - - cx.spawn(async move |this, cx| { - if let Err(err) = task.await { - this.update(cx, |this, cx| { - this.handle_thread_error(err, cx); - }) - .ok(); - } else { - this.update(cx, |this, cx| { - this.should_be_following = this - .workspace - .update(cx, |workspace, _| { - workspace.is_being_followed(CollaboratorId::Agent) - }) - .unwrap_or_default(); - }) - .ok(); - } - }) - .detach(); + self.message_set_from_history = Self::set_draft_message( + self.message_editor.clone(), + self.mention_set.clone(), + self.project.clone(), + self.message_history.borrow_mut().prev(), + window, + cx, + ); } - fn cancel_editing(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { - let Some(thread) = self.thread().cloned() else { - return; - }; - - if let Some(index) = self.editing_message.take() - && let Some(editor) = self - .entry_view_state - .read(cx) - .entry(index) - .and_then(|e| e.message_editor()) - .cloned() - { - editor.update(cx, |editor, cx| { - if let Some(user_message) = thread - .read(cx) - .entries() - .get(index) - .and_then(|e| e.user_message()) - { - editor.set_message(user_message.chunks.clone(), window, cx); - } - }) - }; - self.focus_handle(cx).focus(window); - cx.notify(); - } - - fn regenerate( + fn next_history_message( &mut self, - entry_ix: usize, - message_editor: &Entity, + _: &NextHistoryMessage, window: &mut Window, cx: &mut Context, ) { - let Some(thread) = self.thread().cloned() else { - return; - }; - if self.is_loading_contents { - return; - } - - let Some(user_message_id) = thread.update(cx, |thread, _| { - thread.entries().get(entry_ix)?.user_message()?.id.clone() - }) else { - return; - }; - - let contents = message_editor.update(cx, |message_editor, cx| message_editor.contents(cx)); - - let task = cx.spawn(async move |_, cx| { - let contents = contents.await?; - thread - .update(cx, |thread, cx| thread.rewind(user_message_id, cx))? - .await?; - Ok(contents) - }); - self.send_impl(task, window, cx); + self.message_set_from_history = Self::set_draft_message( + self.message_editor.clone(), + self.mention_set.clone(), + self.project.clone(), + self.message_history.borrow_mut().next(), + window, + cx, + ); } fn open_agent_diff(&mut self, _: &OpenAgentDiff, window: &mut Window, cx: &mut Context) { @@ -1057,50 +462,87 @@ impl AcpThreadView { }; diff.update(cx, |diff, cx| { - diff.move_to_path(PathKey::for_buffer(buffer, cx), window, cx) + diff.move_to_path(PathKey::for_buffer(&buffer, cx), window, cx) }) } - fn handle_open_rules(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { - let Some(thread) = self.as_native_thread(cx) else { - return; + fn set_draft_message( + message_editor: Entity, + mention_set: Arc>, + project: Entity, + message: Option<&Vec>, + window: &mut Window, + cx: &mut Context, + ) -> bool { + cx.notify(); + + let Some(message) = message else { + return false; }; - let project_context = thread.read(cx).project_context().read(cx); - let project_entry_ids = project_context - .worktrees - .iter() - .flat_map(|worktree| worktree.rules_file.as_ref()) - .map(|rules_file| ProjectEntryId::from_usize(rules_file.project_entry_id)) - .collect::>(); + let mut text = String::new(); + let mut mentions = Vec::new(); - self.workspace - .update(cx, move |workspace, cx| { - // TODO: Open a multibuffer instead? In some cases this doesn't make the set of rules - // files clear. For example, if rules file 1 is already open but rules file 2 is not, - // this would open and focus rules file 2 in a tab that is not next to rules file 1. - let project = workspace.project().read(cx); - let project_paths = project_entry_ids - .into_iter() - .flat_map(|entry_id| project.path_for_entry(entry_id, cx)) - .collect::>(); - for project_path in project_paths { - workspace - .open_path(project_path, None, true, window, cx) - .detach_and_log_err(cx); + for chunk in message { + match chunk { + acp::ContentBlock::Text(text_content) => { + text.push_str(&text_content.text); } - }) - .ok(); - } + acp::ContentBlock::ResourceLink(resource_link) => { + let path = Path::new(&resource_link.uri); + let start = text.len(); + let content = MentionPath::new(&path).to_string(); + text.push_str(&content); + let end = text.len(); + if let Some(project_path) = + project.read(cx).project_path_for_absolute_path(&path, cx) + { + let filename: SharedString = path + .file_name() + .unwrap_or_default() + .to_string_lossy() + .to_string() + .into(); + mentions.push((start..end, project_path, filename)); + } + } + acp::ContentBlock::Image(_) + | acp::ContentBlock::Audio(_) + | acp::ContentBlock::Resource(_) => {} + } + } - fn handle_thread_error(&mut self, error: anyhow::Error, cx: &mut Context) { - self.thread_error = Some(ThreadError::from_err(error, &self.agent)); - cx.notify(); - } + let snapshot = message_editor.update(cx, |editor, cx| { + editor.set_text(text, window, cx); + editor.buffer().read(cx).snapshot(cx) + }); - fn clear_thread_error(&mut self, cx: &mut Context) { - self.thread_error = None; - cx.notify(); + for (range, project_path, filename) in mentions { + let crease_icon_path = if project_path.path.is_dir() { + FileIcons::get_folder_icon(false, cx) + .unwrap_or_else(|| IconName::Folder.path().into()) + } else { + FileIcons::get_icon(Path::new(project_path.path.as_ref()), cx) + .unwrap_or_else(|| IconName::File.path().into()) + }; + + let anchor = snapshot.anchor_before(range.start); + let crease_id = crate::context_picker::insert_crease_for_mention( + anchor.excerpt_id, + anchor.text_anchor, + range.end - range.start, + filename, + crease_icon_path, + message_editor.clone(), + window, + cx, + ); + if let Some(crease_id) = crease_id { + mention_set.lock().insert(crease_id, project_path); + } + } + + true } fn handle_thread_event( @@ -1110,192 +552,126 @@ impl AcpThreadView { window: &mut Window, cx: &mut Context, ) { + let count = self.list_state.item_count(); match event { AcpThreadEvent::NewEntry => { - let len = thread.read(cx).entries().len(); - let index = len - 1; - self.entry_view_state.update(cx, |view_state, cx| { - view_state.sync_entry(index, thread, window, cx) - }); - self.list_state.splice(index..index, 1); + let index = thread.read(cx).entries().len() - 1; + self.sync_thread_entry_view(index, window, cx); + self.list_state.splice(count..count, 1); } AcpThreadEvent::EntryUpdated(index) => { - self.entry_view_state.update(cx, |view_state, cx| { - view_state.sync_entry(*index, thread, window, cx) - }); + let index = *index; + self.sync_thread_entry_view(index, window, cx); + self.list_state.splice(index..index + 1, 1); } - AcpThreadEvent::EntriesRemoved(range) => { - self.entry_view_state - .update(cx, |view_state, _cx| view_state.remove(range.clone())); - self.list_state.splice(range.clone(), 0); - } - AcpThreadEvent::ToolAuthorizationRequired => { - self.notify_with_sound("Waiting for tool confirmation", IconName::Info, window, cx); - } - AcpThreadEvent::Retry(retry) => { - self.thread_retry_status = Some(retry.clone()); - } - AcpThreadEvent::Stopped => { - self.thread_retry_status.take(); - let used_tools = thread.read(cx).used_tools_since_last_user_message(); - self.notify_with_sound( - if used_tools { - "Finished running tools" - } else { - "New message" - }, - IconName::ZedAssistant, - window, - cx, - ); - } - AcpThreadEvent::Error => { - self.thread_retry_status.take(); - self.notify_with_sound( - "Agent stopped due to an error", - IconName::Warning, - window, - cx, - ); - } - AcpThreadEvent::LoadError(error) => { - self.thread_retry_status.take(); - self.thread_state = ThreadState::LoadError(error.clone()); - if self.message_editor.focus_handle(cx).is_focused(window) { - self.focus_handle.focus(window) - } - } - AcpThreadEvent::TitleUpdated => { - let title = thread.read(cx).title(); - if let Some(title_editor) = self.title_editor() { - title_editor.update(cx, |editor, cx| { - if editor.text(cx) != title { - editor.set_text(title, window, cx); - } - }); - } - } - AcpThreadEvent::PromptCapabilitiesUpdated => { - self.prompt_capabilities - .set(thread.read(cx).prompt_capabilities()); - } - AcpThreadEvent::TokenUsageUpdated => {} } cx.notify(); } - fn authenticate( + fn sync_thread_entry_view( &mut self, - method: acp::AuthMethodId, + entry_ix: usize, window: &mut Window, cx: &mut Context, ) { - let ThreadState::Unauthenticated { - connection, - pending_auth_method, - configuration_view, - .. - } = &mut self.thread_state - else { + let Some(multibuffers) = self.entry_diff_multibuffers(entry_ix, cx) else { return; }; - if method.0.as_ref() == "gemini-api-key" { - let registry = LanguageModelRegistry::global(cx); - let provider = registry - .read(cx) - .provider(&language_model::GOOGLE_PROVIDER_ID) - .unwrap(); - if !provider.is_authenticated(cx) { - let this = cx.weak_entity(); - let agent = self.agent.clone(); - let connection = connection.clone(); - window.defer(cx, |window, cx| { - Self::handle_auth_required( - this, - AuthRequired { - description: Some("GEMINI_API_KEY must be set".to_owned()), - provider_id: Some(language_model::GOOGLE_PROVIDER_ID), - }, - agent, - connection, - window, - cx, - ); - }); + let multibuffers = multibuffers.collect::>(); + + for multibuffer in multibuffers { + if self.diff_editors.contains_key(&multibuffer.entity_id()) { return; } - } else if method.0.as_ref() == "vertex-ai" - && std::env::var("GOOGLE_API_KEY").is_err() - && (std::env::var("GOOGLE_CLOUD_PROJECT").is_err() - || (std::env::var("GOOGLE_CLOUD_PROJECT").is_err())) - { - let this = cx.weak_entity(); - let agent = self.agent.clone(); - let connection = connection.clone(); - window.defer(cx, |window, cx| { - Self::handle_auth_required( - this, - AuthRequired { - description: Some( - "GOOGLE_API_KEY must be set in the environment to use Vertex AI authentication for Gemini CLI. Please export it and restart Zed." - .to_owned(), - ), - provider_id: None, - }, - agent, - connection, - window, - cx, - ) + let 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, + }, + 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(TextStyleRefinement { + font_size: Some( + TextSize::Small + .rems(cx) + .to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx)) + .into(), + ), + ..Default::default() }); - return; + editor + }); + let entity_id = multibuffer.entity_id(); + cx.observe_release(&multibuffer, move |this, _, _| { + this.diff_editors.remove(&entity_id); + }) + .detach(); + + self.diff_editors.insert(entity_id, editor); } + } - self.thread_error.take(); - configuration_view.take(); - pending_auth_method.replace(method.clone()); - let authenticate = connection.authenticate(method, cx); - cx.notify(); - self.auth_task = - Some(cx.spawn_in(window, { - let project = self.project.clone(); - let agent = self.agent.clone(); - async move |this, cx| { - let result = authenticate.await; + fn entry_diff_multibuffers( + &self, + entry_ix: usize, + cx: &App, + ) -> Option>> { + let entry = self.thread()?.read(cx).entries().get(entry_ix)?; + Some(entry.diffs().map(|diff| diff.multibuffer.clone())) + } - match &result { - Ok(_) => telemetry::event!( - "Authenticate Agent Succeeded", - agent = agent.telemetry_id() - ), - Err(_) => { - telemetry::event!( - "Authenticate Agent Failed", - agent = agent.telemetry_id(), - ) - } + fn authenticate(&mut self, window: &mut Window, cx: &mut Context) { + let ThreadState::Unauthenticated { ref connection } = self.thread_state else { + return; + }; + + self.last_error.take(); + let authenticate = connection.authenticate(cx); + self.auth_task = Some(cx.spawn_in(window, { + let project = self.project.clone(); + let agent = self.agent.clone(); + async move |this, cx| { + let result = authenticate.await; + + this.update_in(cx, |this, window, cx| { + if let Err(err) = result { + this.last_error = Some(cx.new(|cx| { + Markdown::new(format!("Error: {err}").into(), None, None, cx) + })) + } else { + this.thread_state = Self::initial_state( + agent, + this.workspace.clone(), + project.clone(), + window, + cx, + ) } - - this.update_in(cx, |this, window, cx| { - if let Err(err) = result { - this.handle_thread_error(err, cx); - } else { - this.thread_state = Self::initial_state( - agent, - None, - this.workspace.clone(), - project.clone(), - window, - cx, - ) - } - this.auth_task.take() - }) - .ok(); - } - })); + this.auth_task.take() + }) + .ok(); + } + })); } fn authorize_tool_call( @@ -1303,7 +679,6 @@ impl AcpThreadView { tool_call_id: acp::ToolCallId, option_id: acp::PermissionOptionId, option_kind: acp::PermissionOptionKind, - window: &mut Window, cx: &mut Context, ) { let Some(thread) = self.thread() else { @@ -1312,212 +687,41 @@ impl AcpThreadView { thread.update(cx, |thread, cx| { thread.authorize_tool_call(tool_call_id, option_id, option_kind, cx); }); - if self.should_be_following { - self.workspace - .update(cx, |workspace, cx| { - workspace.follow(CollaboratorId::Agent, window, cx); - }) - .ok(); - } - cx.notify(); - } - - fn rewind(&mut self, message_id: &UserMessageId, cx: &mut Context) { - let Some(thread) = self.thread() else { - return; - }; - thread - .update(cx, |thread, cx| thread.rewind(message_id.clone(), cx)) - .detach_and_log_err(cx); cx.notify(); } fn render_entry( &self, - entry_ix: usize, + index: usize, total_entries: usize, entry: &AgentThreadEntry, window: &mut Window, cx: &Context, ) -> AnyElement { - let is_generating = self - .thread() - .is_some_and(|thread| thread.read(cx).status() != ThreadStatus::Idle); - - let primary = match &entry { - AgentThreadEntry::UserMessage(message) => { - let Some(editor) = self - .entry_view_state - .read(cx) - .entry(entry_ix) - .and_then(|entry| entry.message_editor()) - .cloned() - else { - return Empty.into_any_element(); - }; - - let editing = self.editing_message == Some(entry_ix); - let editor_focus = editor.focus_handle(cx).is_focused(window); - let focus_border = cx.theme().colors().border_focused; - - let rules_item = if entry_ix == 0 { - self.render_rules_item(cx) - } else { - None - }; - - let has_checkpoint_button = message - .checkpoint - .as_ref() - .is_some_and(|checkpoint| checkpoint.show); - - let agent_name = self.agent.name(); - - v_flex() - .id(("user_message", entry_ix)) - .map(|this| { - if entry_ix == 0 && !has_checkpoint_button && rules_item.is_none() { - this.pt_4() - } else if rules_item.is_some() { - this.pt_3() - } else { - this.pt_2() - } - }) - .pb_4() - .px_2() - .gap_1p5() - .w_full() - .children(rules_item) - .children(message.id.clone().and_then(|message_id| { - message.checkpoint.as_ref()?.show.then(|| { - h_flex() - .px_3() - .gap_2() - .child(Divider::horizontal()) - .child( - Button::new("restore-checkpoint", "Restore Checkpoint") - .icon(IconName::Undo) - .icon_size(IconSize::XSmall) - .icon_position(IconPosition::Start) - .label_size(LabelSize::XSmall) - .icon_color(Color::Muted) - .color(Color::Muted) - .on_click(cx.listener(move |this, _, _window, cx| { - this.rewind(&message_id, cx); - })) - ) - .child(Divider::horizontal()) - }) - })) - .child( - div() - .relative() - .child( - div() - .py_3() - .px_2() - .rounded_md() - .shadow_md() - .bg(cx.theme().colors().editor_background) - .border_1() - .when(editing && !editor_focus, |this| this.border_dashed()) - .border_color(cx.theme().colors().border) - .map(|this|{ - if editing && editor_focus { - this.border_color(focus_border) - } else if message.id.is_some() { - this.hover(|s| s.border_color(focus_border.opacity(0.8))) - } else { - this - } - }) - .text_xs() - .child(editor.clone().into_any_element()), + match &entry { + AgentThreadEntry::UserMessage(message) => div() + .py_4() + .px_2() + .child( + v_flex() + .p_3() + .gap_1p5() + .rounded_lg() + .shadow_md() + .bg(cx.theme().colors().editor_background) + .border_1() + .border_color(cx.theme().colors().border) + .text_xs() + .children(message.content.markdown().map(|md| { + self.render_markdown( + md.clone(), + user_message_markdown_style(window, cx), ) - .when(editor_focus, |this| { - let base_container = h_flex() - .absolute() - .top_neg_3p5() - .right_3() - .gap_1() - .rounded_sm() - .border_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().editor_background) - .overflow_hidden(); - - if message.id.is_some() { - this.child( - base_container - .child( - IconButton::new("cancel", IconName::Close) - .disabled(self.is_loading_contents) - .icon_color(Color::Error) - .icon_size(IconSize::XSmall) - .on_click(cx.listener(Self::cancel_editing)) - ) - .child( - if self.is_loading_contents { - div() - .id("loading-edited-message-content") - .tooltip(Tooltip::text("Loading Added Context…")) - .child(loading_contents_spinner(IconSize::XSmall)) - .into_any_element() - } else { - IconButton::new("regenerate", IconName::Return) - .icon_color(Color::Muted) - .icon_size(IconSize::XSmall) - .tooltip(Tooltip::text( - "Editing will restart the thread from this point." - )) - .on_click(cx.listener({ - let editor = editor.clone(); - move |this, _, window, cx| { - this.regenerate( - entry_ix, &editor, window, cx, - ); - } - })).into_any_element() - } - ) - ) - } else { - this.child( - base_container - .border_dashed() - .child( - IconButton::new("editing_unavailable", IconName::PencilUnavailable) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .style(ButtonStyle::Transparent) - .tooltip(move |_window, cx| { - cx.new(|_| UnavailableEditingTooltip::new(agent_name.clone())) - .into() - }) - ) - ) - } - }), - ) - .into_any() - } + })), + ) + .into_any(), AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => { - let is_last = entry_ix + 1 == total_entries; - let pending_thinking_chunk_ix = if is_generating && is_last { - chunks - .iter() - .enumerate() - .next_back() - .filter(|(_, segment)| { - matches!(segment, AssistantMessageChunk::Thought { .. }) - }) - .map(|(index, _)| index) - } else { - None - }; - - let style = default_markdown_style(false, false, window, cx); + let style = default_markdown_style(false, window, cx); let message_body = v_flex() .w_full() .gap_2p5() @@ -1532,10 +736,9 @@ impl AcpThreadView { AssistantMessageChunk::Thought { block } => { block.markdown().map(|md| { self.render_thinking_block( - entry_ix, + index, chunk_ix, md.clone(), - Some(chunk_ix) == pending_thinking_chunk_ix, window, cx, ) @@ -1549,68 +752,17 @@ impl AcpThreadView { v_flex() .px_5() .py_1() - .when(is_last, |this| this.pb_4()) + .when(index + 1 == total_entries, |this| this.pb_4()) .w_full() .text_ui(cx) .child(message_body) .into_any() } - AgentThreadEntry::ToolCall(tool_call) => { - let has_terminals = tool_call.terminals().next().is_some(); - - div().w_full().map(|this| { - if has_terminals { - this.children(tool_call.terminals().map(|terminal| { - self.render_terminal_tool_call( - entry_ix, terminal, tool_call, window, cx, - ) - })) - } else { - this.child(self.render_tool_call(entry_ix, tool_call, window, cx)) - } - }) - } - .into_any(), - }; - - let Some(thread) = self.thread() else { - return primary; - }; - - let primary = if entry_ix == total_entries - 1 { - v_flex() - .w_full() - .child(primary) - .child(self.render_thread_controls(&thread, cx)) - .when_some( - self.thread_feedback.comments_editor.clone(), - |this, editor| this.child(Self::render_feedback_feedback_editor(editor, cx)), - ) - .into_any_element() - } else { - primary - }; - - if let Some(editing_index) = self.editing_message.as_ref() - && *editing_index < entry_ix - { - let backdrop = div() - .id(("backdrop", entry_ix)) - .size_full() - .absolute() - .inset_0() - .bg(cx.theme().colors().panel_background) - .opacity(0.8) - .block_mouse_except_scroll() - .on_click(cx.listener(Self::cancel_editing)); - - div() - .relative() - .child(primary) - .child(backdrop) - .into_any_element() - } else { - primary + AgentThreadEntry::ToolCall(tool_call) => div() + .py_1p5() + .px_5() + .child(self.render_tool_call(index, tool_call, window, cx)) + .into_any(), } } @@ -1622,7 +774,7 @@ impl AcpThreadView { } fn tool_card_border_color(&self, cx: &Context) -> Hsla { - cx.theme().colors().border.opacity(0.8) + cx.theme().colors().border.opacity(0.6) } fn tool_name_font_size(&self) -> Rems { @@ -1634,90 +786,52 @@ impl AcpThreadView { entry_ix: usize, chunk_ix: usize, chunk: Entity, - pending: bool, window: &Window, cx: &Context, ) -> AnyElement { let header_id = SharedString::from(format!("thinking-block-header-{}", entry_ix)); - let card_header_id = SharedString::from("inner-card-header"); - let key = (entry_ix, chunk_ix); - let is_open = self.expanded_thinking_blocks.contains(&key); - let editor_bg = cx.theme().colors().editor_background; - let gradient_overlay = div() - .rounded_b_lg() - .h_full() - .absolute() - .w_full() - .bottom_0() - .left_0() - .bg(linear_gradient( - 180., - linear_color_stop(editor_bg, 1.), - linear_color_stop(editor_bg.opacity(0.2), 0.), - )); - - let scroll_handle = self - .entry_view_state - .read(cx) - .entry(entry_ix) - .and_then(|entry| entry.scroll_handle_for_assistant_message_chunk(chunk_ix)); v_flex() - .rounded_md() - .border_1() - .border_color(self.tool_card_border_color(cx)) .child( h_flex() .id(header_id) - .group(&card_header_id) - .relative() + .group("disclosure-header") .w_full() - .py_0p5() - .px_1p5() - .rounded_t_md() - .bg(self.tool_card_header_bg(cx)) .justify_between() - .border_b_1() - .border_color(self.tool_card_border_color(cx)) + .opacity(0.8) + .hover(|style| style.opacity(1.)) .child( h_flex() - .h(window.line_height()) .gap_1p5() .child( - Icon::new(IconName::ToolThink) + Icon::new(IconName::ToolBulb) .size(IconSize::Small) .color(Color::Muted), ) .child( div() .text_size(self.tool_name_font_size()) - .text_color(cx.theme().colors().text_muted) - .map(|this| { - if pending { - this.child("Thinking") - } else { - this.child("Thought Process") - } - }), + .child("Thinking"), ), ) .child( - Disclosure::new(("expand", entry_ix), is_open) - .opened_icon(IconName::ChevronUp) - .closed_icon(IconName::ChevronDown) - .visible_on_hover(&card_header_id) - .on_click(cx.listener({ - move |this, _event, _window, cx| { - if is_open { - this.expanded_thinking_blocks.remove(&key); - } else { - this.expanded_thinking_blocks.insert(key); + div().visible_on_hover("disclosure-header").child( + Disclosure::new("thinking-disclosure", is_open) + .opened_icon(IconName::ChevronUp) + .closed_icon(IconName::ChevronDown) + .on_click(cx.listener({ + move |this, _event, _window, cx| { + if is_open { + this.expanded_thinking_blocks.remove(&key); + } else { + this.expanded_thinking_blocks.insert(key); + } + cx.notify(); } - cx.notify(); - } - })), + })), + ), ) .on_click(cx.listener({ move |this, _event, _window, cx| { @@ -1730,28 +844,21 @@ impl AcpThreadView { } })), ) - .child( - div() - .relative() - .bg(editor_bg) - .rounded_b_lg() - .child( - div() - .id(("thinking-content", chunk_ix)) - .when_some(scroll_handle, |this, scroll_handle| { - this.track_scroll(&scroll_handle) - }) - .p_2() - .when(!is_open, |this| this.max_h_20()) - .text_ui_sm(cx) - .overflow_hidden() - .child(self.render_markdown( - chunk, - default_markdown_style(false, false, window, cx), - )), - ) - .when(!is_open && pending, |this| this.child(gradient_overlay)), - ) + .when(is_open, |this| { + this.child( + div() + .relative() + .mt_1p5() + .ml(px(7.)) + .pl_4() + .border_l_1() + .border_color(self.tool_card_border_color(cx)) + .text_ui_sm(cx) + .child( + self.render_markdown(chunk, default_markdown_style(false, window, cx)), + ), + ) + }) .into_any_element() } @@ -1762,174 +869,109 @@ impl AcpThreadView { window: &Window, cx: &Context, ) -> Div { - let card_header_id = SharedString::from("inner-tool-call-header"); + let header_id = SharedString::from(format!("tool-call-header-{}", entry_ix)); - let tool_icon = - if tool_call.kind == acp::ToolKind::Edit && tool_call.locations.len() == 1 { - FileIcons::get_icon(&tool_call.locations[0].path, cx) - .map(Icon::from_path) - .unwrap_or(Icon::new(IconName::ToolPencil)) - } else { - Icon::new(match tool_call.kind { - acp::ToolKind::Read => IconName::ToolSearch, - acp::ToolKind::Edit => IconName::ToolPencil, - acp::ToolKind::Delete => IconName::ToolDeleteFile, - acp::ToolKind::Move => IconName::ArrowRightLeft, - acp::ToolKind::Search => IconName::ToolSearch, - acp::ToolKind::Execute => IconName::ToolTerminal, - acp::ToolKind::Think => IconName::ToolThink, - acp::ToolKind::Fetch => IconName::ToolWeb, - acp::ToolKind::Other => IconName::ToolHammer, - }) + let status_icon = match &tool_call.status { + ToolCallStatus::Allowed { + status: acp::ToolCallStatus::Pending, } - .size(IconSize::Small) - .color(Color::Muted); - - let failed_or_canceled = match &tool_call.status { - ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed => true, - _ => false, + | ToolCallStatus::WaitingForConfirmation { .. } => None, + ToolCallStatus::Allowed { + status: acp::ToolCallStatus::InProgress, + .. + } => Some( + Icon::new(IconName::ArrowCircle) + .color(Color::Accent) + .size(IconSize::Small) + .with_animation( + "running", + Animation::new(Duration::from_secs(2)).repeat(), + |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), + ) + .into_any(), + ), + ToolCallStatus::Allowed { + status: acp::ToolCallStatus::Completed, + .. + } => None, + ToolCallStatus::Rejected + | ToolCallStatus::Canceled + | ToolCallStatus::Allowed { + status: acp::ToolCallStatus::Failed, + .. + } => Some( + Icon::new(IconName::X) + .color(Color::Error) + .size(IconSize::Small) + .into_any_element(), + ), }; - let has_location = tool_call.locations.len() == 1; - let needs_confirmation = matches!( - tool_call.status, - ToolCallStatus::WaitingForConfirmation { .. } - ); - let is_edit = - matches!(tool_call.kind, acp::ToolKind::Edit) || tool_call.diffs().next().is_some(); - let use_card_layout = needs_confirmation || is_edit; + let needs_confirmation = match &tool_call.status { + ToolCallStatus::WaitingForConfirmation { .. } => true, + _ => tool_call + .content + .iter() + .any(|content| matches!(content, ToolCallContent::Diff { .. })), + }; let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation; - - let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id); - - let gradient_overlay = { - div() - .absolute() - .top_0() - .right_0() - .w_12() - .h_full() - .map(|this| { - if use_card_layout { - this.bg(linear_gradient( - 90., - linear_color_stop(self.tool_card_header_bg(cx), 1.), - linear_color_stop(self.tool_card_header_bg(cx).opacity(0.2), 0.), - )) - } else { - this.bg(linear_gradient( - 90., - linear_color_stop(cx.theme().colors().panel_background, 1.), - linear_color_stop( - cx.theme().colors().panel_background.opacity(0.2), - 0., - ), - )) - } - }) - }; - - let tool_output_display = if is_open { - match &tool_call.status { - ToolCallStatus::WaitingForConfirmation { options, .. } => { - v_flex() - .w_full() - .children(tool_call.content.iter().map(|content| { - div() - .child(self.render_tool_call_content( - entry_ix, content, tool_call, window, cx, - )) - .into_any_element() - })) - .child(self.render_permission_buttons( - options, - entry_ix, - tool_call.id.clone(), - tool_call.content.is_empty(), - cx, - )) - .into_any() - } - ToolCallStatus::Pending | ToolCallStatus::InProgress - if is_edit - && tool_call.content.is_empty() - && self.as_native_connection(cx).is_some() => - { - self.render_diff_loading(cx).into_any() - } - ToolCallStatus::Pending - | ToolCallStatus::InProgress - | ToolCallStatus::Completed - | ToolCallStatus::Failed - | ToolCallStatus::Canceled => v_flex() - .w_full() - .children(tool_call.content.iter().map(|content| { - div().child( - self.render_tool_call_content(entry_ix, content, tool_call, window, cx), - ) - })) - .into_any(), - ToolCallStatus::Rejected => Empty.into_any(), - } - .into() - } else { - None - }; + let is_open = !is_collapsible || self.expanded_tool_calls.contains(&tool_call.id); v_flex() - .map(|this| { - if use_card_layout { - this.my_2() - .rounded_md() - .border_1() - .border_color(self.tool_card_border_color(cx)) - .bg(cx.theme().colors().editor_background) - .overflow_hidden() - } else { - this.my_1() - } + .when(needs_confirmation, |this| { + this.rounded_lg() + .border_1() + .border_color(self.tool_card_border_color(cx)) + .bg(cx.theme().colors().editor_background) + .overflow_hidden() }) - .map(|this| { - if has_location && !use_card_layout { - this.ml_4() - } else { - this.ml_5() - } - }) - .mr_5() .child( h_flex() - .group(&card_header_id) - .relative() + .id(header_id) .w_full() .gap_1() .justify_between() - .when(use_card_layout, |this| { - this.p_0p5() - .rounded_t_md() - .bg(self.tool_card_header_bg(cx)) - .when(is_open && !failed_or_canceled, |this| { - this.border_b_1() - .border_color(self.tool_card_border_color(cx)) - }) + .map(|this| { + if needs_confirmation { + this.px_2() + .py_1() + .rounded_t_md() + .bg(self.tool_card_header_bg(cx)) + .border_b_1() + .border_color(self.tool_card_border_color(cx)) + } else { + this.opacity(0.8).hover(|style| style.opacity(1.)) + } }) .child( h_flex() - .relative() - .w_full() - .h(window.line_height()) - .text_size(self.tool_name_font_size()) - .gap_1p5() - .when(has_location || use_card_layout, |this| this.px_1()) - .when(has_location, |this| { - this.cursor(CursorStyle::PointingHand) - .rounded_sm() - .hover(|s| s.bg(cx.theme().colors().element_hover.opacity(0.5))) + .id("tool-call-header") + .overflow_x_scroll() + .map(|this| { + if needs_confirmation { + this.text_xs() + } else { + this.text_size(self.tool_name_font_size()) + } }) - .overflow_hidden() - .child(tool_icon) - .child(if has_location { + .gap_1p5() + .child( + Icon::new(match tool_call.kind { + acp::ToolKind::Read => IconName::ToolRead, + acp::ToolKind::Edit => IconName::ToolPencil, + acp::ToolKind::Delete => IconName::ToolDeleteFile, + acp::ToolKind::Move => IconName::ArrowRightLeft, + acp::ToolKind::Search => IconName::ToolSearch, + acp::ToolKind::Execute => IconName::ToolTerminal, + acp::ToolKind::Think => IconName::ToolBulb, + acp::ToolKind::Fetch => IconName::ToolWeb, + acp::ToolKind::Other => IconName::ToolHammer, + }) + .size(IconSize::Small) + .color(Color::Muted), + ) + .child(if tool_call.locations.len() == 1 { let name = tool_call.locations[0] .path .file_name() @@ -1939,42 +981,42 @@ impl AcpThreadView { h_flex() .id(("open-tool-call-location", entry_ix)) - .w_full() - .map(|this| { - if use_card_layout { - this.text_color(cx.theme().colors().text) - } else { - this.text_color(cx.theme().colors().text_muted) - } - }) .child(name) + .w_full() + .max_w_full() + .pr_1() + .gap_0p5() + .cursor_pointer() + .rounded_sm() + .opacity(0.8) + .hover(|label| { + label.opacity(1.).bg(cx + .theme() + .colors() + .element_hover + .opacity(0.5)) + }) .tooltip(Tooltip::text("Jump to File")) .on_click(cx.listener(move |this, _, window, cx| { this.open_tool_call_location(entry_ix, 0, window, cx); })) .into_any_element() } else { - h_flex() - .w_full() - .child(self.render_markdown( - tool_call.label.clone(), - default_markdown_style(false, true, window, cx), - )) - .into_any() - }) - .when(!has_location, |this| this.child(gradient_overlay)), + self.render_markdown( + tool_call.label.clone(), + default_markdown_style(needs_confirmation, window, cx), + ) + .into_any() + }), ) - .when(is_collapsible || failed_or_canceled, |this| { - this.child( - h_flex() - .px_1() - .gap_px() - .when(is_collapsible, |this| { - this.child( + .child( + h_flex() + .gap_0p5() + .when(is_collapsible, |this| { + this.child( Disclosure::new(("expand", entry_ix), is_open) .opened_icon(IconName::ChevronUp) .closed_icon(IconName::ChevronDown) - .visible_on_hover(&card_header_id) .on_click(cx.listener({ let id = tool_call.id.clone(); move |this: &mut Self, _, _, cx: &mut Context| { @@ -1987,136 +1029,102 @@ impl AcpThreadView { } })), ) - }) - .when(failed_or_canceled, |this| { - this.child( - Icon::new(IconName::Close) - .color(Color::Error) - .size(IconSize::Small), - ) - }), - ) - }), - ) - .children(tool_output_display) - } - - fn render_tool_call_content( - &self, - entry_ix: usize, - content: &ToolCallContent, - tool_call: &ToolCall, - window: &Window, - cx: &Context, - ) -> AnyElement { - match content { - ToolCallContent::ContentBlock(content) => { - if let Some(resource_link) = content.resource_link() { - self.render_resource_link(resource_link, cx) - } else if let Some(markdown) = content.markdown() { - self.render_markdown_output(markdown.clone(), tool_call.id.clone(), window, cx) - } else { - Empty.into_any_element() - } - } - ToolCallContent::Diff(diff) => self.render_diff_editor(entry_ix, diff, tool_call, cx), - ToolCallContent::Terminal(terminal) => { - self.render_terminal_tool_call(entry_ix, terminal, tool_call, window, cx) - } - } - } - - fn render_markdown_output( - &self, - markdown: Entity, - tool_call_id: acp::ToolCallId, - window: &Window, - cx: &Context, - ) -> AnyElement { - let button_id = SharedString::from(format!("tool_output-{:?}", tool_call_id)); - - v_flex() - .mt_1p5() - .ml(rems(0.4)) - .px_3p5() - .gap_2() - .border_l_1() - .border_color(self.tool_card_border_color(cx)) - .text_sm() - .text_color(cx.theme().colors().text_muted) - .child(self.render_markdown(markdown, default_markdown_style(false, false, window, cx))) - .child( - IconButton::new(button_id, IconName::ChevronUp) - .full_width() - .style(ButtonStyle::Outlined) - .icon_color(Color::Muted) + }) + .children(status_icon), + ) .on_click(cx.listener({ + let id = tool_call.id.clone(); move |this: &mut Self, _, _, cx: &mut Context| { - this.expanded_tool_calls.remove(&tool_call_id); + if is_open { + this.expanded_tool_calls.remove(&id); + } else { + this.expanded_tool_calls.insert(id.clone()); + } cx.notify(); } })), ) - .into_any_element() + .when(is_open, |this| { + this.child( + v_flex() + .text_xs() + .when(is_collapsible, |this| { + this.mt_1() + .border_1() + .border_color(self.tool_card_border_color(cx)) + .bg(cx.theme().colors().editor_background) + .rounded_lg() + }) + .map(|this| { + if is_open { + match &tool_call.status { + ToolCallStatus::WaitingForConfirmation { options, .. } => this + .children(tool_call.content.iter().map(|content| { + div() + .py_1p5() + .child( + self.render_tool_call_content( + content, window, cx, + ), + ) + .into_any_element() + })) + .child(self.render_permission_buttons( + options, + entry_ix, + tool_call.id.clone(), + tool_call.content.is_empty(), + cx, + )), + ToolCallStatus::Allowed { .. } | ToolCallStatus::Canceled => { + this.children(tool_call.content.iter().map(|content| { + div() + .py_1p5() + .child( + self.render_tool_call_content( + content, window, cx, + ), + ) + .into_any_element() + })) + } + ToolCallStatus::Rejected => this, + } + } else { + this + } + }), + ) + }) } - fn render_resource_link( + fn render_tool_call_content( &self, - resource_link: &acp::ResourceLink, + content: &ToolCallContent, + window: &Window, cx: &Context, ) -> AnyElement { - let uri: SharedString = resource_link.uri.clone().into(); - let is_file = resource_link.uri.strip_prefix("file://"); - - let label: SharedString = if let Some(abs_path) = is_file { - if let Some(project_path) = self - .project - .read(cx) - .project_path_for_absolute_path(&Path::new(abs_path), cx) - && let Some(worktree) = self - .project - .read(cx) - .worktree_for_id(project_path.worktree_id, cx) - { - worktree - .read(cx) - .full_path(&project_path.path) - .to_string_lossy() - .to_string() - .into() - } else { - abs_path.to_string().into() + match content { + ToolCallContent::ContentBlock { content } => { + if let Some(md) = content.markdown() { + div() + .p_2() + .child( + self.render_markdown( + md.clone(), + default_markdown_style(false, window, cx), + ), + ) + .into_any_element() + } else { + Empty.into_any_element() + } } - } else { - uri.clone() - }; - - let button_id = SharedString::from(format!("item-{}", uri)); - - div() - .ml(rems(0.4)) - .pl_2p5() - .border_l_1() - .border_color(self.tool_card_border_color(cx)) - .overflow_hidden() - .child( - Button::new(button_id, label) - .label_size(LabelSize::Small) - .color(Color::Muted) - .truncate(true) - .when(is_file.is_none(), |this| { - this.icon(IconName::ArrowUpRight) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - }) - .on_click(cx.listener({ - let workspace = self.workspace.clone(); - move |_, _, window, cx: &mut Context| { - Self::open_link(uri.clone(), &workspace, window, cx); - } - })), - ) - .into_any_element() + ToolCallContent::Diff { + diff: Diff { multibuffer, .. }, + .. + } => self.render_diff_editor(multibuffer), + } } fn render_permission_buttons( @@ -2128,24 +1136,17 @@ impl AcpThreadView { cx: &Context, ) -> Div { h_flex() - .py_1() - .pl_2() - .pr_1() + .py_1p5() + .px_1p5() .gap_1() - .justify_between() - .flex_wrap() + .justify_end() .when(!empty_content, |this| { this.border_t_1() .border_color(self.tool_card_border_color(cx)) }) - .child( - div() - .min_w(rems_from_px(145.)) - .child(LoadingLabel::new("Waiting for Confirmation").size(LabelSize::Small)), - ) - .child(h_flex().gap_0p5().children(options.iter().map(|option| { + .children(options.iter().map(|option| { let option_id = SharedString::from(option.id.0.clone()); - Button::new((option_id, entry_ix), option.name.clone()) + Button::new((option_id, entry_ix), option.label.clone()) .map(|this| match option.kind { acp::PermissionOptionKind::AllowOnce => { this.icon(IconName::Check).icon_color(Color::Success) @@ -2154,95 +1155,36 @@ impl AcpThreadView { this.icon(IconName::CheckDouble).icon_color(Color::Success) } acp::PermissionOptionKind::RejectOnce => { - this.icon(IconName::Close).icon_color(Color::Error) + this.icon(IconName::X).icon_color(Color::Error) } acp::PermissionOptionKind::RejectAlways => { - this.icon(IconName::Close).icon_color(Color::Error) + this.icon(IconName::X).icon_color(Color::Error) } }) .icon_position(IconPosition::Start) .icon_size(IconSize::XSmall) - .label_size(LabelSize::Small) .on_click(cx.listener({ let tool_call_id = tool_call_id.clone(); let option_id = option.id.clone(); let option_kind = option.kind; - move |this, _, window, cx| { + move |this, _, _, cx| { this.authorize_tool_call( tool_call_id.clone(), option_id.clone(), option_kind, - window, cx, ); } })) - }))) + })) } - fn render_diff_loading(&self, cx: &Context) -> AnyElement { - let bar = |n: u64, width_class: &str| { - let bg_color = cx.theme().colors().element_active; - let base = h_flex().h_1().rounded_full(); - - let modified = match width_class { - "w_4_5" => base.w_3_4(), - "w_1_4" => base.w_1_4(), - "w_2_4" => base.w_2_4(), - "w_3_5" => base.w_3_5(), - "w_2_5" => base.w_2_5(), - _ => base.w_1_2(), - }; - - modified.with_animation( - ElementId::Integer(n), - Animation::new(Duration::from_secs(2)).repeat(), - move |tab, delta| { - let delta = (delta - 0.15 * n as f32) / 0.7; - let delta = 1.0 - (0.5 - delta).abs() * 2.; - let delta = ease_in_out(delta.clamp(0., 1.)); - let delta = 0.1 + 0.9 * delta; - - tab.bg(bg_color.opacity(delta)) - }, - ) - }; - - v_flex() - .p_3() - .gap_1() - .rounded_b_md() - .bg(cx.theme().colors().editor_background) - .child(bar(0, "w_4_5")) - .child(bar(1, "w_1_4")) - .child(bar(2, "w_2_4")) - .child(bar(3, "w_3_5")) - .child(bar(4, "w_2_5")) - .into_any_element() - } - - fn render_diff_editor( - &self, - entry_ix: usize, - diff: &Entity, - tool_call: &ToolCall, - cx: &Context, - ) -> AnyElement { - let tool_progress = matches!( - &tool_call.status, - ToolCallStatus::InProgress | ToolCallStatus::Pending - ); - + fn render_diff_editor(&self, multibuffer: &Entity) -> AnyElement { v_flex() .h_full() .child( - if let Some(entry) = self.entry_view_state.read(cx).entry(entry_ix) - && let Some(editor) = entry.editor_for_diff(diff) - && diff.read(cx).has_revealed_range(cx) - { - editor.into_any_element() - } else if tool_progress && self.as_native_connection(cx).is_some() { - self.render_diff_loading(cx) + if let Some(editor) = self.diff_editors.get(&multibuffer.entity_id()) { + editor.clone().into_any_element() } else { Empty.into_any() }, @@ -2250,747 +1192,160 @@ impl AcpThreadView { .into_any() } - fn render_terminal_tool_call( - &self, - entry_ix: usize, - terminal: &Entity, - tool_call: &ToolCall, - window: &Window, - cx: &Context, - ) -> AnyElement { - let terminal_data = terminal.read(cx); - let working_dir = terminal_data.working_dir(); - let command = terminal_data.command(); - let started_at = terminal_data.started_at(); - - let tool_failed = matches!( - &tool_call.status, - ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed - ); - - let output = terminal_data.output(); - let command_finished = output.is_some(); - let truncated_output = output.is_some_and(|output| output.was_content_truncated); - let output_line_count = output.map(|output| output.content_line_count).unwrap_or(0); - - let command_failed = command_finished - && output.is_some_and(|o| o.exit_status.is_none_or(|status| !status.success())); - - let time_elapsed = if let Some(output) = output { - output.ended_at.duration_since(started_at) - } else { - started_at.elapsed() - }; - - let header_id = - SharedString::from(format!("terminal-tool-header-{}", terminal.entity_id())); - let header_group = SharedString::from(format!( - "terminal-tool-header-group-{}", - terminal.entity_id() - )); - let header_bg = cx - .theme() - .colors() - .element_background - .blend(cx.theme().colors().editor_foreground.opacity(0.025)); - let border_color = cx.theme().colors().border.opacity(0.6); - - let working_dir = working_dir - .as_ref() - .map(|path| format!("{}", path.display())) - .unwrap_or_else(|| "current directory".to_string()); - - let is_expanded = self.expanded_tool_calls.contains(&tool_call.id); - - let header = h_flex() - .id(header_id) - .flex_none() - .gap_1() - .justify_between() - .rounded_t_md() - .child( - div() - .id(("command-target-path", terminal.entity_id())) - .w_full() - .max_w_full() - .overflow_x_scroll() - .child( - Label::new(working_dir) - .buffer_font(cx) - .size(LabelSize::XSmall) - .color(Color::Muted), - ), - ) - .when(!command_finished, |header| { - header - .gap_1p5() - .child( - Button::new( - SharedString::from(format!("stop-terminal-{}", terminal.entity_id())), - "Stop", - ) - .icon(IconName::Stop) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .icon_color(Color::Error) - .label_size(LabelSize::Small) - .tooltip(move |window, cx| { - Tooltip::with_meta( - "Stop This Command", - None, - "Also possible by placing your cursor inside the terminal and using regular terminal bindings.", - window, - cx, - ) - }) - .on_click({ - let terminal = terminal.clone(); - cx.listener(move |_this, _event, _window, cx| { - let inner_terminal = terminal.read(cx).inner().clone(); - inner_terminal.update(cx, |inner_terminal, _cx| { - inner_terminal.kill_active_task(); - }); - }) - }), - ) - .child(Divider::vertical()) - .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))) - }, - ), - ) - }) - .when(truncated_output, |header| { - let tooltip = if let Some(output) = output { - if output_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES { - "Output exceeded terminal max lines and was \ - truncated, the model received the first 16 KB." - .to_string() - } else { - format!( - "Output is {} long, and to avoid unexpected token usage, \ - only 16 KB was sent back to the model.", - format_file_size(output.original_content_len as u64, true), - ) - } - } else { - "Output was truncated".to_string() - }; - - header.child( - h_flex() - .id(("terminal-tool-truncated-label", terminal.entity_id())) - .gap_1() - .child( - Icon::new(IconName::Info) - .size(IconSize::XSmall) - .color(Color::Ignored), - ) - .child( - Label::new("Truncated") - .color(Color::Muted) - .size(LabelSize::XSmall), - ) - .tooltip(Tooltip::text(tooltip)), - ) - }) - .when(time_elapsed > Duration::from_secs(10), |header| { - header.child( - Label::new(format!("({})", duration_alt_display(time_elapsed))) - .buffer_font(cx) - .color(Color::Muted) - .size(LabelSize::XSmall), - ) - }) - .child( - Disclosure::new( - SharedString::from(format!( - "terminal-tool-disclosure-{}", - terminal.entity_id() - )), - is_expanded, - ) - .opened_icon(IconName::ChevronUp) - .closed_icon(IconName::ChevronDown) - .visible_on_hover(&header_group) - .on_click(cx.listener({ - let id = tool_call.id.clone(); - move |this, _event, _window, _cx| { - if is_expanded { - this.expanded_tool_calls.remove(&id); - } else { - this.expanded_tool_calls.insert(id.clone()); - } - } - })), - ) - .when(tool_failed || command_failed, |header| { - header.child( - div() - .id(("terminal-tool-error-code-indicator", terminal.entity_id())) - .child( - Icon::new(IconName::Close) - .size(IconSize::Small) - .color(Color::Error), - ) - .when_some(output.and_then(|o| o.exit_status), |this, status| { - this.tooltip(Tooltip::text(format!( - "Exited with code {}", - status.code().unwrap_or(-1), - ))) - }), - ) - }); - - let terminal_view = self - .entry_view_state - .read(cx) - .entry(entry_ix) - .and_then(|entry| entry.terminal(terminal)); - let show_output = is_expanded && terminal_view.is_some(); - - v_flex() - .my_2() - .mx_5() - .border_1() - .when(tool_failed || command_failed, |card| card.border_dashed()) - .border_color(border_color) - .rounded_md() - .overflow_hidden() - .child( - v_flex() - .group(&header_group) - .py_1p5() - .pr_1p5() - .pl_2() - .gap_0p5() - .bg(header_bg) - .text_xs() - .child(header) - .child( - MarkdownElement::new( - command.clone(), - terminal_command_markdown_style(window, cx), - ) - .code_block_renderer( - markdown::CodeBlockRenderer::Default { - copy_button: false, - copy_button_on_hover: true, - border: false, - }, - ), - ), - ) - .when(show_output, |this| { - this.child( - div() - .pt_2() - .border_t_1() - .when(tool_failed || command_failed, |card| card.border_dashed()) - .border_color(border_color) - .bg(cx.theme().colors().editor_background) - .rounded_b_md() - .text_ui_sm(cx) - .children(terminal_view.clone()), - ) - }) - .into_any() + fn render_agent_logo(&self) -> AnyElement { + Icon::new(self.agent.logo()) + .color(Color::Muted) + .size(IconSize::XLarge) + .into_any_element() } - fn render_rules_item(&self, cx: &Context) -> Option { - let project_context = self - .as_native_thread(cx)? - .read(cx) - .project_context() - .read(cx); + fn render_error_agent_logo(&self) -> AnyElement { + let logo = Icon::new(self.agent.logo()) + .color(Color::Muted) + .size(IconSize::XLarge) + .into_any_element(); - let user_rules_text = if project_context.user_rules.is_empty() { - None - } else if project_context.user_rules.len() == 1 { - let user_rules = &project_context.user_rules[0]; - - match user_rules.title.as_ref() { - Some(title) => Some(format!("Using \"{title}\" user rule")), - None => Some("Using user rule".into()), - } - } else { - Some(format!( - "Using {} user rules", - project_context.user_rules.len() - )) - }; - - let first_user_rules_id = project_context - .user_rules - .first() - .map(|user_rules| user_rules.uuid.0); - - let rules_files = project_context - .worktrees - .iter() - .filter_map(|worktree| worktree.rules_file.as_ref()) - .collect::>(); - - let rules_file_text = match rules_files.as_slice() { - &[] => None, - &[rules_file] => Some(format!( - "Using project {:?} file", - rules_file.path_in_worktree - )), - rules_files => Some(format!("Using {} project rules files", rules_files.len())), - }; - - if user_rules_text.is_none() && rules_file_text.is_none() { - return None; - } - - let has_both = user_rules_text.is_some() && rules_file_text.is_some(); - - Some( - h_flex() - .px_2p5() - .child( - Icon::new(IconName::Attach) - .size(IconSize::XSmall) - .color(Color::Disabled), - ) - .when_some(user_rules_text, |parent, user_rules_text| { - parent.child( - h_flex() - .id("user-rules") - .ml_1() - .mr_1p5() - .child( - Label::new(user_rules_text) - .size(LabelSize::XSmall) - .color(Color::Muted) - .truncate(), - ) - .hover(|s| s.bg(cx.theme().colors().element_hover)) - .tooltip(Tooltip::text("View User Rules")) - .on_click(move |_event, window, cx| { - window.dispatch_action( - Box::new(OpenRulesLibrary { - prompt_to_select: first_user_rules_id, - }), - cx, - ) - }), - ) - }) - .when(has_both, |this| { - this.child( - Label::new("•") - .size(LabelSize::XSmall) - .color(Color::Disabled), - ) - }) - .when_some(rules_file_text, |parent, rules_file_text| { - parent.child( - h_flex() - .id("project-rules") - .ml_1p5() - .child( - Label::new(rules_file_text) - .size(LabelSize::XSmall) - .color(Color::Muted), - ) - .hover(|s| s.bg(cx.theme().colors().element_hover)) - .tooltip(Tooltip::text("View Project Rules")) - .on_click(cx.listener(Self::handle_open_rules)), - ) - }) - .into_any(), - ) + h_flex() + .relative() + .justify_center() + .child(div().opacity(0.3).child(logo)) + .child( + h_flex().absolute().right_1().bottom_0().child( + Icon::new(IconName::XCircle) + .color(Color::Error) + .size(IconSize::Small), + ), + ) + .into_any_element() } - fn render_empty_state_section_header( - &self, - label: impl Into, - action_slot: Option, - cx: &mut Context, - ) -> impl IntoElement { - div().pl_1().pr_1p5().child( - h_flex() - .mt_2() - .pl_1p5() - .pb_1() - .w_full() - .justify_between() - .border_b_1() - .border_color(cx.theme().colors().border_variant) - .child( - Label::new(label.into()) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .children(action_slot), - ) - } - - fn render_recent_history(&self, window: &mut Window, cx: &mut Context) -> AnyElement { - let render_history = self - .agent - .clone() - .downcast::() - .is_some() - && self - .history_store - .update(cx, |history_store, cx| !history_store.is_empty(cx)); + fn render_empty_state(&self, cx: &App) -> AnyElement { + let loading = matches!(&self.thread_state, ThreadState::Loading { .. }); v_flex() .size_full() - .when(render_history, |this| { - let recent_history: Vec<_> = self.history_store.update(cx, |history_store, _| { - history_store.entries().take(3).collect() - }); - this.justify_end().child( - v_flex() - .child( - self.render_empty_state_section_header( - "Recent", - Some( - Button::new("view-history", "View All") - .style(ButtonStyle::Subtle) - .label_size(LabelSize::Small) - .key_binding( - KeyBinding::for_action_in( - &OpenHistory, - &self.focus_handle(cx), - window, - cx, - ) - .map(|kb| kb.size(rems_from_px(12.))), - ) - .on_click(move |_event, window, cx| { - window.dispatch_action(OpenHistory.boxed_clone(), cx); - }) - .into_any_element(), - ), - cx, - ), - ) - .child( - v_flex().p_1().pr_1p5().gap_1().children( - recent_history - .into_iter() - .enumerate() - .map(|(index, entry)| { - // TODO: Add keyboard navigation. - let is_hovered = - self.hovered_recent_history_item == Some(index); - crate::acp::thread_history::AcpHistoryEntryElement::new( - entry, - cx.entity().downgrade(), - ) - .hovered(is_hovered) - .on_hover(cx.listener( - move |this, is_hovered, _window, cx| { - if *is_hovered { - this.hovered_recent_history_item = Some(index); - } else if this.hovered_recent_history_item - == Some(index) - { - this.hovered_recent_history_item = None; - } - cx.notify(); - }, - )) - .into_any_element() - }), - ), - ), - ) + .items_center() + .justify_center() + .child(if loading { + h_flex() + .justify_center() + .child(self.render_agent_logo()) + .with_animation( + "pulsating_icon", + Animation::new(Duration::from_secs(2)) + .repeat() + .with_easing(pulsating_between(0.4, 1.0)), + |icon, delta| icon.opacity(delta), + ) + .into_any() + } else { + self.render_agent_logo().into_any_element() }) + .child(h_flex().mt_4().mb_1().justify_center().child(if loading { + div() + .child(LoadingLabel::new("").size(LabelSize::Large)) + .into_any_element() + } else { + Headline::new(self.agent.empty_state_headline()) + .size(HeadlineSize::Medium) + .into_any_element() + })) + .child( + div() + .max_w_1_2() + .text_sm() + .text_center() + .map(|this| { + if loading { + this.invisible() + } else { + this.text_color(cx.theme().colors().text_muted) + } + }) + .child(self.agent.empty_state_message()), + ) .into_any() } - fn render_auth_required_state( - &self, - connection: &Rc, - description: Option<&Entity>, - configuration_view: Option<&AnyView>, - pending_auth_method: Option<&acp::AuthMethodId>, - window: &mut Window, - cx: &Context, - ) -> Div { - let show_description = - configuration_view.is_none() && description.is_none() && pending_auth_method.is_none(); - - v_flex().flex_1().size_full().justify_end().child( - v_flex() - .p_2() - .pr_3() - .w_full() - .gap_1() - .border_t_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().status().warning.opacity(0.04)) - .child( - h_flex() - .gap_1p5() - .child( - Icon::new(IconName::Warning) - .color(Color::Warning) - .size(IconSize::Small), - ) - .child(Label::new("Authentication Required").size(LabelSize::Small)), - ) - .children(description.map(|desc| { - div().text_ui(cx).child(self.render_markdown( - desc.clone(), - default_markdown_style(false, false, window, cx), - )) - })) - .children( - configuration_view - .cloned() - .map(|view| div().w_full().child(view)), - ) - .when( - show_description, - |el| { - el.child( - Label::new(format!( - "You are not currently authenticated with {}. Please choose one of the following options:", - self.agent.name() - )) - .size(LabelSize::Small) - .color(Color::Muted) - .mb_1() - .ml_5(), - ) - }, - ) - .when_some(pending_auth_method, |el, _| { - el.child( - h_flex() - .py_4() - .w_full() - .justify_center() - .gap_1() - .child( - Icon::new(IconName::ArrowCircle) - .size(IconSize::Small) - .color(Color::Muted) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| { - icon.transform(Transformation::rotate(percentage( - delta, - ))) - }, - ) - .into_any_element(), - ) - .child(Label::new("Authenticating…").size(LabelSize::Small)), - ) - }) - .when(!connection.auth_methods().is_empty(), |this| { - this.child( - h_flex() - .justify_end() - .flex_wrap() - .gap_1() - .when(!show_description, |this| { - this.border_t_1() - .mt_1() - .pt_2() - .border_color(cx.theme().colors().border.opacity(0.8)) - }) - .children( - connection - .auth_methods() - .iter() - .enumerate() - .rev() - .map(|(ix, method)| { - Button::new( - SharedString::from(method.id.0.clone()), - method.name.clone(), - ) - .when(ix == 0, |el| { - el.style(ButtonStyle::Tinted(ui::TintColor::Warning)) - }) - .label_size(LabelSize::Small) - .on_click({ - let method_id = method.id.clone(); - cx.listener(move |this, _, window, cx| { - telemetry::event!( - "Authenticate Agent Started", - agent = this.agent.telemetry_id(), - method = method_id - ); - - this.authenticate(method_id.clone(), window, cx) - }) - }) - }), - ), - ) - }) - - ) - } - - fn render_load_error( - &self, - e: &LoadError, - window: &mut Window, - cx: &mut Context, - ) -> AnyElement { - let (message, action_slot): (SharedString, _) = match e { - LoadError::NotInstalled { - error_message: _, - install_message: _, - install_command, - } => { - return self.render_not_installed(install_command.clone(), false, window, cx); - } - LoadError::Unsupported { - error_message: _, - upgrade_message: _, - upgrade_command, - } => { - return self.render_not_installed(upgrade_command.clone(), true, window, cx); - } - LoadError::Exited { .. } => ("Server exited with status {status}".into(), None), - LoadError::Other(msg) => ( - msg.into(), - Some(self.create_copy_button(msg.to_string()).into_any_element()), - ), - }; - - Callout::new() - .severity(Severity::Error) - .icon(IconName::XCircleFilled) - .title("Failed to Launch") - .description(message) - .actions_slot(div().children(action_slot)) - .into_any_element() - } - - fn install_agent(&self, install_command: String, window: &mut Window, cx: &mut Context) { - telemetry::event!("Agent Install CLI", agent = self.agent.telemetry_id()); - let task = self - .workspace - .update(cx, |workspace, cx| { - let project = workspace.project().read(cx); - let cwd = project.first_project_directory(cx); - let shell = project.terminal_settings(&cwd, cx).shell.clone(); - let spawn_in_terminal = task::SpawnInTerminal { - id: task::TaskId(install_command.clone()), - full_label: install_command.clone(), - label: install_command.clone(), - command: Some(install_command.clone()), - args: Vec::new(), - command_label: install_command.clone(), - cwd, - env: Default::default(), - use_new_terminal: true, - allow_concurrent_runs: true, - reveal: Default::default(), - reveal_target: Default::default(), - hide: Default::default(), - shell, - show_summary: true, - show_command: true, - show_rerun: false, - }; - workspace.spawn_in_terminal(spawn_in_terminal, window, cx) - }) - .ok(); - let Some(task) = task else { return }; - cx.spawn_in(window, async move |this, cx| { - if let Some(Ok(_)) = task.await { - this.update_in(cx, |this, window, cx| { - this.reset(window, cx); - }) - .ok(); - } - }) - .detach() - } - - fn render_not_installed( - &self, - install_command: String, - is_upgrade: bool, - window: &mut Window, - cx: &mut Context, - ) -> AnyElement { - self.install_command_markdown.update(cx, |markdown, cx| { - if !markdown.source().contains(&install_command) { - markdown.replace(format!("```\n{}\n```", install_command), cx); - } - }); - - let (heading_label, description_label, button_label, or_label) = if is_upgrade { - ( - "Upgrade Gemini CLI in Zed", - "Get access to the latest version with support for Zed.", - "Upgrade Gemini CLI", - "Or, to upgrade it manually:", - ) - } else { - ( - "Get Started with Gemini CLI in Zed", - "Use Google's new coding agent directly in Zed.", - "Install Gemini CLI", - "Or, to install it manually:", - ) - }; - + fn render_pending_auth_state(&self) -> AnyElement { v_flex() - .w_full() - .p_3p5() - .gap_2p5() - .border_t_1() - .border_color(cx.theme().colors().border) - .bg(linear_gradient( - 180., - linear_color_stop(cx.theme().colors().editor_background.opacity(0.4), 4.), - linear_color_stop(cx.theme().status().info_background.opacity(0.), 0.), - )) + .items_center() + .justify_center() + .child(self.render_error_agent_logo()) .child( - v_flex().gap_0p5().child(Label::new(heading_label)).child( - Label::new(description_label) - .size(LabelSize::Small) - .color(Color::Muted), - ), + h_flex() + .mt_4() + .mb_1() + .justify_center() + .child(Headline::new("Not Authenticated").size(HeadlineSize::Medium)), ) + .into_any() + } + + fn render_error_state(&self, e: &LoadError, cx: &Context) -> AnyElement { + let mut container = v_flex() + .items_center() + .justify_center() + .child(self.render_error_agent_logo()) .child( - Button::new("install_gemini", button_label) - .full_width() - .size(ButtonSize::Medium) - .style(ButtonStyle::Tinted(TintColor::Accent)) - .label_size(LabelSize::Small) - .icon(IconName::TerminalGhost) - .icon_color(Color::Muted) - .icon_size(IconSize::Small) - .icon_position(IconPosition::Start) - .on_click(cx.listener(move |this, _, window, cx| { - this.install_agent(install_command.clone(), window, cx) - })), - ) - .child( - Label::new(or_label) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .child(MarkdownElement::new( - self.install_command_markdown.clone(), - default_markdown_style(false, false, window, cx), - )) - .into_any_element() + v_flex() + .mt_4() + .mb_2() + .gap_0p5() + .text_center() + .items_center() + .child(Headline::new("Failed to launch").size(HeadlineSize::Medium)) + .child( + Label::new(e.to_string()) + .size(LabelSize::Small) + .color(Color::Muted), + ), + ); + + if let LoadError::Unsupported { + upgrade_message, + upgrade_command, + .. + } = &e + { + let upgrade_message = upgrade_message.clone(); + let upgrade_command = upgrade_command.clone(); + container = container.child(Button::new("upgrade", upgrade_message).on_click( + cx.listener(move |this, _, window, cx| { + this.workspace + .update(cx, |workspace, cx| { + let project = workspace.project().read(cx); + let cwd = project.first_project_directory(cx); + let shell = project.terminal_settings(&cwd, cx).shell.clone(); + let spawn_in_terminal = task::SpawnInTerminal { + id: task::TaskId("install".to_string()), + full_label: upgrade_command.clone(), + label: upgrade_command.clone(), + command: Some(upgrade_command.clone()), + args: Vec::new(), + command_label: upgrade_command.clone(), + cwd, + env: Default::default(), + use_new_terminal: true, + allow_concurrent_runs: true, + reveal: Default::default(), + reveal_target: Default::default(), + hide: Default::default(), + shell, + show_summary: true, + show_command: true, + show_rerun: false, + }; + workspace + .spawn_in_terminal(spawn_in_terminal, window, cx) + .detach(); + }) + .ok(); + }), + )); + } + + container.into_any() } fn render_activity_bar( @@ -3034,25 +1389,24 @@ impl AcpThreadView { parent.child(self.render_plan_entries(plan, window, cx)) }) }) - .when(!plan.is_empty() && !changed_buffers.is_empty(), |this| { - this.child(Divider::horizontal().color(DividerColor::Border)) - }) .when(!changed_buffers.is_empty(), |this| { - this.child(self.render_edits_summary( - &changed_buffers, - self.edits_expanded, - pending_edits, - window, - cx, - )) - .when(self.edits_expanded, |parent| { - parent.child(self.render_edited_files( + this.child(Divider::horizontal()) + .child(self.render_edits_summary( action_log, &changed_buffers, + self.edits_expanded, pending_edits, + window, cx, )) - }) + .when(self.edits_expanded, |parent| { + parent.child(self.render_edited_files( + action_log, + &changed_buffers, + pending_edits, + cx, + )) + }) }) .into_any() .into() @@ -3066,7 +1420,6 @@ impl AcpThreadView { { h_flex() .w_full() - .cursor_default() .gap_1() .text_xs() .text_color(cx.theme().colors().text_muted) @@ -3096,7 +1449,7 @@ impl AcpThreadView { let status_label = if stats.pending == 0 { "All Done".to_string() } else if stats.completed == 0 { - format!("{} Tasks", plan.entries.len()) + format!("{}", plan.entries.len()) } else { format!("{}/{}", stats.completed, plan.entries.len()) }; @@ -3190,6 +1543,7 @@ impl AcpThreadView { fn render_edits_summary( &self, + action_log: &Entity, changed_buffers: &BTreeMap, Entity>, expanded: bool, pending_edits: bool, @@ -3203,13 +1557,14 @@ impl AcpThreadView { h_flex() .p_1() .justify_between() - .flex_wrap() .when(expanded, |this| { this.border_b_1().border_color(cx.theme().colors().border) }) .child( h_flex() .id("edits-container") + .cursor_pointer() + .w_full() .gap_1() .child(Disclosure::new("edits-disclosure", expanded)) .map(|this| { @@ -3300,9 +1655,14 @@ impl AcpThreadView { ) .map(|kb| kb.size(rems_from_px(10.))), ) - .on_click(cx.listener(move |this, _, window, cx| { - this.reject_all(&RejectAll, window, cx); - })), + .on_click({ + let action_log = action_log.clone(); + cx.listener(move |_, _, _, cx| { + action_log.update(cx, |action_log, cx| { + action_log.reject_all_edits(cx).detach(); + }) + }) + }), ) .child( Button::new("keep-all-changes", "Keep All") @@ -3315,9 +1675,14 @@ impl AcpThreadView { KeyBinding::for_action_in(&KeepAll, &focus_handle, window, cx) .map(|kb| kb.size(rems_from_px(10.))), ) - .on_click(cx.listener(move |this, _, window, cx| { - this.keep_all(&KeepAll, window, cx); - })), + .on_click({ + let action_log = action_log.clone(); + cx.listener(move |_, _, _, cx| { + action_log.update(cx, |action_log, cx| { + action_log.keep_all_edits(cx); + }) + }) + }), ), ) } @@ -3331,7 +1696,7 @@ impl AcpThreadView { ) -> Div { let editor_bg_color = cx.theme().colors().editor_background; - v_flex().children(changed_buffers.iter().enumerate().flat_map( + v_flex().children(changed_buffers.into_iter().enumerate().flat_map( |(index, (buffer, _diff))| { let file = buffer.read(cx).file()?; let path = file.path(); @@ -3357,7 +1722,7 @@ impl AcpThreadView { .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(|| { @@ -3481,32 +1846,8 @@ impl AcpThreadView { (IconName::Maximize, "Expand Message Editor") }; - let backdrop = div() - .size_full() - .absolute() - .inset_0() - .bg(cx.theme().colors().panel_background) - .opacity(0.8) - .block_mouse_except_scroll(); - - let enable_editor = match self.thread_state { - ThreadState::Loading { .. } | ThreadState::Ready { .. } => true, - ThreadState::Unauthenticated { .. } | ThreadState::LoadError(..) => false, - }; - v_flex() .on_action(cx.listener(Self::expand_message_editor)) - .on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| { - if let Some(profile_selector) = this.profile_selector.as_ref() { - profile_selector.read(cx).menu_handle().toggle(window, cx); - } - })) - .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| { - if let Some(model_selector) = this.model_selector.as_ref() { - model_selector - .update(cx, |model_selector, cx| model_selector.toggle(window, cx)); - } - })) .p_2() .gap_2() .border_t_1() @@ -3521,7 +1862,34 @@ impl AcpThreadView { .size_full() .pt_1() .pr_2p5() - .child(self.message_editor.clone()) + .child(div().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.message_editor, + EditorStyle { + background: editor_bg_color, + local_player: cx.theme().players().local(), + text: text_style, + syntax: cx.theme().syntax().clone(), + ..Default::default() + }, + ) + })) .child( h_flex() .absolute() @@ -3531,9 +1899,10 @@ impl AcpThreadView { .hover(|this| this.opacity(1.0)) .child( IconButton::new("toggle-height", expand_icon) - .icon_size(IconSize::Small) + .icon_size(IconSize::XSmall) .icon_color(Color::Muted) .tooltip({ + let focus_handle = focus_handle.clone(); move |window, cx| { Tooltip::for_action_in( expand_tooltip, @@ -3553,258 +1922,51 @@ impl AcpThreadView { .child( h_flex() .flex_none() - .flex_wrap() .justify_between() - .child( - h_flex() - .child(self.render_follow_toggle(cx)) - .children(self.render_burn_mode_toggle(cx)), - ) - .child( - h_flex() - .gap_1() - .children(self.render_token_usage(cx)) - .children(self.profile_selector.clone()) - .children(self.model_selector.clone()) - .child(self.render_send_button(cx)), - ), + .child(self.render_follow_toggle(cx)) + .child(self.render_send_button(cx)), ) - .when(!enable_editor, |this| this.child(backdrop)) .into_any() } - pub(crate) fn as_native_connection( - &self, - cx: &App, - ) -> Option> { - let acp_thread = self.thread()?.read(cx); - acp_thread.connection().clone().downcast() - } - - pub(crate) fn as_native_thread(&self, cx: &App) -> Option> { - let acp_thread = self.thread()?.read(cx); - self.as_native_connection(cx)? - .thread(acp_thread.session_id(), cx) - } - - fn is_using_zed_ai_models(&self, cx: &App) -> bool { - self.as_native_thread(cx) - .and_then(|thread| thread.read(cx).model()) - .is_some_and(|model| model.provider_id() == language_model::ZED_CLOUD_PROVIDER_ID) - } - - fn render_token_usage(&self, cx: &mut Context) -> Option
{ - let thread = self.thread()?.read(cx); - let usage = thread.token_usage()?; - let is_generating = thread.status() != ThreadStatus::Idle; - - let used = crate::text_thread_editor::humanize_token_count(usage.used_tokens); - let max = crate::text_thread_editor::humanize_token_count(usage.max_tokens); - - Some( - h_flex() - .flex_shrink_0() - .gap_0p5() - .mr_1p5() - .child( - Label::new(used) - .size(LabelSize::Small) - .color(Color::Muted) - .map(|label| { - if is_generating { - label - .with_animation( - "used-tokens-label", - Animation::new(Duration::from_secs(2)) - .repeat() - .with_easing(pulsating_between(0.3, 0.8)), - |label, delta| label.alpha(delta), - ) - .into_any() - } else { - label.into_any_element() - } - }), - ) - .child( - Label::new("/") - .size(LabelSize::Small) - .color(Color::Custom(cx.theme().colors().text_muted.opacity(0.5))), - ) - .child(Label::new(max).size(LabelSize::Small).color(Color::Muted)), - ) - } - - fn toggle_burn_mode( - &mut self, - _: &ToggleBurnMode, - _window: &mut Window, - cx: &mut Context, - ) { - let Some(thread) = self.as_native_thread(cx) else { - return; - }; - - thread.update(cx, |thread, cx| { - let current_mode = thread.completion_mode(); - thread.set_completion_mode( - match current_mode { - CompletionMode::Burn => CompletionMode::Normal, - CompletionMode::Normal => CompletionMode::Burn, - }, - cx, - ); - }); - } - - fn keep_all(&mut self, _: &KeepAll, _window: &mut Window, cx: &mut Context) { - let Some(thread) = self.thread() else { - return; - }; - let action_log = thread.read(cx).action_log().clone(); - action_log.update(cx, |action_log, cx| action_log.keep_all_edits(cx)); - } - - fn reject_all(&mut self, _: &RejectAll, _window: &mut Window, cx: &mut Context) { - let Some(thread) = self.thread() else { - return; - }; - let action_log = thread.read(cx).action_log().clone(); - action_log - .update(cx, |action_log, cx| action_log.reject_all_edits(cx)) - .detach(); - } - - fn render_burn_mode_toggle(&self, cx: &mut Context) -> Option { - let thread = self.as_native_thread(cx)?.read(cx); - - if thread - .model() - .is_none_or(|model| !model.supports_burn_mode()) - { - return None; - } - - let active_completion_mode = thread.completion_mode(); - let burn_mode_enabled = active_completion_mode == CompletionMode::Burn; - let icon = if burn_mode_enabled { - IconName::ZedBurnModeOn - } else { - IconName::ZedBurnMode - }; - - Some( - IconButton::new("burn-mode", icon) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .toggle_state(burn_mode_enabled) - .selected_icon_color(Color::Error) - .on_click(cx.listener(|this, _event, window, cx| { - this.toggle_burn_mode(&ToggleBurnMode, window, cx); - })) - .tooltip(move |_window, cx| { - cx.new(|_| BurnModeTooltip::new().selected(burn_mode_enabled)) - .into() - }) - .into_any_element(), - ) - } - fn render_send_button(&self, cx: &mut Context) -> AnyElement { - let is_editor_empty = self.message_editor.read(cx).is_empty(cx); - let is_generating = self - .thread() - .is_some_and(|thread| thread.read(cx).status() != ThreadStatus::Idle); - - if self.is_loading_contents { - div() - .id("loading-message-content") - .px_1() - .tooltip(Tooltip::text("Loading Added Context…")) - .child(loading_contents_spinner(IconSize::default())) + if self.thread().map_or(true, |thread| { + thread.read(cx).status() == ThreadStatus::Idle + }) { + let is_editor_empty = self.message_editor.read(cx).is_empty(cx); + IconButton::new("send-message", IconName::Send) + .icon_color(Color::Accent) + .style(ButtonStyle::Filled) + .disabled(self.thread().is_none() || is_editor_empty) + .on_click(cx.listener(|this, _, window, cx| { + this.chat(&Chat, window, cx); + })) + .when(!is_editor_empty, |button| { + button.tooltip(move |window, cx| Tooltip::for_action("Send", &Chat, window, cx)) + }) + .when(is_editor_empty, |button| { + button.tooltip(Tooltip::text("Type a message to submit")) + }) .into_any_element() - } else if is_generating && is_editor_empty { - IconButton::new("stop-generation", IconName::Stop) + } else { + IconButton::new("stop-generation", IconName::StopFilled) .icon_color(Color::Error) .style(ButtonStyle::Tinted(ui::TintColor::Error)) .tooltip(move |window, cx| { Tooltip::for_action("Stop Generation", &editor::actions::Cancel, window, cx) }) - .on_click(cx.listener(|this, _event, _, cx| this.cancel_generation(cx))) - .into_any_element() - } else { - let send_btn_tooltip = if is_editor_empty && !is_generating { - "Type to Send" - } else if is_generating { - "Stop and Send Message" - } else { - "Send" - }; - - IconButton::new("send-message", IconName::Send) - .style(ButtonStyle::Filled) - .map(|this| { - if is_editor_empty && !is_generating { - this.disabled(true).icon_color(Color::Muted) - } else { - this.icon_color(Color::Accent) - } - }) - .tooltip(move |window, cx| Tooltip::for_action(send_btn_tooltip, &Chat, window, cx)) - .on_click(cx.listener(|this, _, window, cx| { - this.send(window, cx); - })) + .on_click(cx.listener(|this, _event, _, cx| this.cancel(cx))) .into_any_element() } } - fn is_following(&self, cx: &App) -> bool { - match self.thread().map(|thread| thread.read(cx).status()) { - Some(ThreadStatus::Generating) => self - .workspace - .read_with(cx, |workspace, _| { - workspace.is_being_followed(CollaboratorId::Agent) - }) - .unwrap_or(false), - _ => self.should_be_following, - } - } - - fn toggle_following(&mut self, window: &mut Window, cx: &mut Context) { - let following = self.is_following(cx); - - self.should_be_following = !following; - if self.thread().map(|thread| thread.read(cx).status()) == Some(ThreadStatus::Generating) { - self.workspace - .update(cx, |workspace, cx| { - if following { - workspace.unfollow(CollaboratorId::Agent, window, cx); - } else { - workspace.follow(CollaboratorId::Agent, window, cx); - } - }) - .ok(); - } - - telemetry::event!("Follow Agent Selected", following = !following); - } - fn render_follow_toggle(&self, cx: &mut Context) -> impl IntoElement { - let following = self.is_following(cx); - - let tooltip_label = if following { - if self.agent.name() == "Zed Agent" { - format!("Stop Following the {}", self.agent.name()) - } else { - format!("Stop Following {}", self.agent.name()) - } - } else { - if self.agent.name() == "Zed Agent" { - format!("Follow the {}", self.agent.name()) - } else { - format!("Follow {}", self.agent.name()) - } - }; + let following = self + .workspace + .read_with(cx, |workspace, _| { + workspace.is_being_followed(CollaboratorId::Agent) + }) + .unwrap_or(false); IconButton::new("follow-agent", IconName::Crosshair) .icon_size(IconSize::Small) @@ -3813,10 +1975,10 @@ impl AcpThreadView { .selected_icon_color(Some(Color::Custom(cx.theme().players().agent().cursor))) .tooltip(move |window, cx| { if following { - Tooltip::for_action(tooltip_label.clone(), &Follow, window, cx) + Tooltip::for_action("Stop Following Agent", &Follow, window, cx) } else { Tooltip::with_meta( - tooltip_label.clone(), + "Follow Agent", Some(&Follow), "Track the agent's location as it reads and edits files.", window, @@ -3825,7 +1987,15 @@ impl AcpThreadView { } }) .on_click(cx.listener(move |this, _, window, cx| { - this.toggle_following(window, cx); + this.workspace + .update(cx, |workspace, cx| { + if following { + workspace.unfollow(CollaboratorId::Agent, window, cx); + } else { + workspace.follow(CollaboratorId::Agent, window, cx); + } + }) + .ok(); })) } @@ -3847,113 +2017,26 @@ impl AcpThreadView { return; }; - if let Some(mention) = MentionUri::parse(&url).log_err() { - workspace.update(cx, |workspace, cx| match mention { - MentionUri::File { abs_path } => { - let project = workspace.project(); - let Some(path) = - project.update(cx, |project, cx| project.find_project_path(abs_path, cx)) - else { - return; - }; - - workspace - .open_path(path, None, true, window, cx) - .detach_and_log_err(cx); - } - MentionUri::PastedImage => {} - MentionUri::Directory { abs_path } => { - let project = workspace.project(); - let Some(entry) = project.update(cx, |project, cx| { - let path = project.find_project_path(abs_path, cx)?; - project.entry_for_path(&path, cx) - }) else { - return; - }; + if let Some(mention_path) = MentionPath::try_parse(&url) { + workspace.update(cx, |workspace, cx| { + let project = workspace.project(); + let Some((path, entry)) = project.update(cx, |project, cx| { + let path = project.find_project_path(mention_path.path(), cx)?; + let entry = project.entry_for_path(&path, cx)?; + Some((path, entry)) + }) else { + return; + }; + if entry.is_dir() { project.update(cx, |_, cx| { cx.emit(project::Event::RevealInProjectPanel(entry.id)); }); - } - MentionUri::Symbol { - abs_path: path, - line_range, - .. - } - | MentionUri::Selection { - abs_path: Some(path), - line_range, - } => { - let project = workspace.project(); - let Some((path, _)) = project.update(cx, |project, cx| { - let path = project.find_project_path(path, cx)?; - let entry = project.entry_for_path(&path, cx)?; - Some((path, entry)) - }) else { - return; - }; - - let item = workspace.open_path(path, None, true, window, cx); - window - .spawn(cx, async move |cx| { - let Some(editor) = item.await?.downcast::() else { - return Ok(()); - }; - let range = Point::new(*line_range.start(), 0) - ..Point::new(*line_range.start(), 0); - editor - .update_in(cx, |editor, window, cx| { - editor.change_selections( - SelectionEffects::scroll(Autoscroll::center()), - window, - cx, - |s| s.select_ranges(vec![range]), - ); - }) - .ok(); - anyhow::Ok(()) - }) + } else { + workspace + .open_path(path, None, true, window, cx) .detach_and_log_err(cx); } - MentionUri::Selection { abs_path: None, .. } => {} - MentionUri::Thread { id, name } => { - if let Some(panel) = workspace.panel::(cx) { - panel.update(cx, |panel, cx| { - panel.load_agent_thread( - DbThreadMetadata { - id, - title: name.into(), - updated_at: Default::default(), - }, - window, - cx, - ) - }); - } - } - MentionUri::TextThread { path, .. } => { - if let Some(panel) = workspace.panel::(cx) { - panel.update(cx, |panel, cx| { - panel - .open_saved_prompt_editor(path.as_path().into(), window, cx) - .detach_and_log_err(cx); - }); - } - } - MentionUri::Rule { id, .. } => { - let PromptId::User { uuid } = id else { - return; - }; - window.dispatch_action( - Box::new(OpenRulesLibrary { - prompt_to_select: Some(uuid.0), - }), - cx, - ) - } - MentionUri::Fetch { url } => { - cx.open_url(url.as_str()); - } }) } else { cx.open_url(&url); @@ -3967,24 +2050,26 @@ impl AcpThreadView { window: &mut Window, cx: &mut Context, ) -> Option<()> { - let (tool_call_location, agent_location) = self + let location = self .thread()? .read(cx) .entries() .get(entry_ix)? - .location(location_ix)?; + .locations()? + .get(location_ix)?; let project_path = self .project .read(cx) - .find_project_path(&tool_call_location.path, cx)?; + .find_project_path(&location.path, cx)?; let open_task = self .workspace - .update(cx, |workspace, cx| { - workspace.open_path(project_path, None, true, window, cx) + .update(cx, |worskpace, cx| { + worskpace.open_path(project_path, None, true, window, cx) }) .log_err()?; + window .spawn(cx, async move |cx| { let item = open_task.await?; @@ -3994,22 +2079,17 @@ impl AcpThreadView { }; active_editor.update_in(cx, |editor, window, cx| { - let multibuffer = editor.buffer().read(cx); - let buffer = multibuffer.as_singleton(); - if agent_location.buffer.upgrade() == buffer { - let excerpt_id = multibuffer.excerpt_ids().first().cloned(); - let anchor = editor::Anchor::in_buffer( - excerpt_id.unwrap(), - buffer.unwrap().read(cx).remote_id(), - agent_location.position, - ); + let snapshot = editor.buffer().read(cx).snapshot(cx); + let first_hunk = editor + .diff_hunks_in_ranges( + &[editor::Anchor::min()..editor::Anchor::max()], + &snapshot, + ) + .next(); + if let Some(first_hunk) = first_hunk { + let first_hunk_start = first_hunk.multi_buffer_range().start; editor.change_selections(Default::default(), window, cx, |selections| { - selections.select_anchor_ranges([anchor..anchor]); - }) - } else { - let row = tool_call_location.line.unwrap_or_default(); - editor.change_selections(Default::default(), window, cx, |selections| { - selections.select_ranges([Point::new(row, 0)..Point::new(row, 0)]); + selections.select_anchor_ranges([first_hunk_start..first_hunk_start]); }) } })?; @@ -4047,7 +2127,7 @@ impl AcpThreadView { let project = workspace.project().clone(); if !project.read(cx).is_local() { - bail!("failed to open active thread as markdown in remote project"); + anyhow::bail!("failed to open active thread as markdown in remote project"); } let buffer = project.update(cx, |project, cx| { @@ -4080,181 +2160,18 @@ impl AcpThreadView { self.list_state.scroll_to(ListOffset::default()); cx.notify(); } +} - pub fn scroll_to_bottom(&mut self, cx: &mut Context) { - if let Some(thread) = self.thread() { - let entry_count = thread.read(cx).entries().len(); - self.list_state.reset(entry_count); - cx.notify(); - } +impl Focusable for AcpThreadView { + fn focus_handle(&self, cx: &App) -> FocusHandle { + self.message_editor.focus_handle(cx) } +} - fn notify_with_sound( - &mut self, - caption: impl Into, - icon: IconName, - window: &mut Window, - cx: &mut Context, - ) { - self.play_notification_sound(window, cx); - self.show_notification(caption, icon, window, cx); - } - - fn play_notification_sound(&self, window: &Window, cx: &mut App) { - let settings = AgentSettings::get_global(cx); - if settings.play_sound_when_agent_done && !window.is_window_active() { - Audio::play_sound(Sound::AgentDone, cx); - } - } - - fn show_notification( - &mut self, - caption: impl Into, - icon: IconName, - window: &mut Window, - cx: &mut Context, - ) { - if window.is_window_active() || !self.notifications.is_empty() { - return; - } - - // TODO: Change this once we have title summarization for external agents. - let title = self.agent.name(); - - 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, window, primary, cx); - } - } - NotifyWhenAgentWaiting::AllScreens => { - let caption = caption.into(); - for screen in cx.displays() { - self.pop_up(icon, caption.clone(), title.clone(), window, screen, cx); - } - } - NotifyWhenAgentWaiting::Never => { - // Don't show anything - } - } - } - - fn pop_up( - &mut self, - icon: IconName, - caption: SharedString, - title: SharedString, - window: &mut Window, - screen: Rc, - cx: &mut Context, - ) { - let options = AgentNotification::window_options(screen, cx); - - let project_name = self.workspace.upgrade().and_then(|workspace| { - workspace - .read(cx) - .project() - .read(cx) - .visible_worktrees(cx) - .next() - .map(|worktree| worktree.read(cx).root_name().to_string()) - }); - - if let Some(screen_window) = cx - .open_window(options, |_, cx| { - cx.new(|_| { - AgentNotification::new(title.clone(), caption.clone(), icon, project_name) - }) - }) - .log_err() - && 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::(window, cx); - }); - } - }) - .log_err(); - }); - - this.dismiss_notifications(cx); - } - 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); - }); - } - }) - }); - } - } - - fn dismiss_notifications(&mut self, cx: &mut Context) { - for window in self.notifications.drain(..) { - window - .update(cx, |_, window, _| { - window.remove_window(); - }) - .ok(); - - self.notification_subscriptions.remove(&window); - } - } - - fn render_thread_controls( - &self, - thread: &Entity, - cx: &Context, - ) -> impl IntoElement { - let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating); - if is_generating { - return h_flex().id("thread-controls-container").child( - div() - .py_2() - .px_5() - .child(SpinnerLabel::new().size(LabelSize::Small)), - ); - } - - let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::Small) +impl Render for AcpThreadView { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let open_as_markdown = IconButton::new("open-as-markdown", IconName::DocumentText) + .icon_size(IconSize::XSmall) .icon_color(Color::Ignored) .tooltip(Tooltip::text("Open Thread as Markdown")) .on_click(cx.listener(move |this, _, window, cx| { @@ -4264,763 +2181,129 @@ impl AcpThreadView { } })); - let scroll_to_top = IconButton::new("scroll_to_top", IconName::ArrowUp) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::Small) + let scroll_to_top = IconButton::new("scroll_to_top", IconName::ArrowUpAlt) + .icon_size(IconSize::XSmall) .icon_color(Color::Ignored) .tooltip(Tooltip::text("Scroll To Top")) .on_click(cx.listener(move |this, _, _, cx| { this.scroll_to_top(cx); })); - let mut container = h_flex() - .id("thread-controls-container") - .group("thread-controls-container") - .w_full() - .py_2() - .px_5() - .gap_px() - .opacity(0.6) - .hover(|style| style.opacity(1.)) - .flex_wrap() - .justify_end(); - - if AgentSettings::get_global(cx).enable_feedback - && self - .thread() - .is_some_and(|thread| thread.read(cx).connection().telemetry().is_some()) - { - let feedback = self.thread_feedback.feedback; - - container = container - .child( - div().visible_on_hover("thread-controls-container").child( - Label::new(match feedback { - Some(ThreadFeedback::Positive) => "Thanks for your feedback!", - Some(ThreadFeedback::Negative) => { - "We appreciate your feedback and will use it to improve." - } - None => { - "Rating the thread sends all of your current conversation to the Zed team." - } - }) - .color(Color::Muted) - .size(LabelSize::XSmall) - .truncate(), - ), - ) - .child( - IconButton::new("feedback-thumbs-up", IconName::ThumbsUp) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::Small) - .icon_color(match feedback { - Some(ThreadFeedback::Positive) => Color::Accent, - _ => Color::Ignored, - }) - .tooltip(Tooltip::text("Helpful Response")) - .on_click(cx.listener(move |this, _, window, cx| { - this.handle_feedback_click(ThreadFeedback::Positive, window, cx); - })), - ) - .child( - IconButton::new("feedback-thumbs-down", IconName::ThumbsDown) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::Small) - .icon_color(match feedback { - Some(ThreadFeedback::Negative) => Color::Accent, - _ => Color::Ignored, - }) - .tooltip(Tooltip::text("Not Helpful")) - .on_click(cx.listener(move |this, _, window, cx| { - this.handle_feedback_click(ThreadFeedback::Negative, window, cx); - })), - ); - } - - container.child(open_as_markdown).child(scroll_to_top) - } - - fn render_feedback_feedback_editor(editor: Entity, cx: &Context) -> Div { - h_flex() - .key_context("AgentFeedbackMessageEditor") - .on_action(cx.listener(move |this, _: &menu::Cancel, _, cx| { - this.thread_feedback.dismiss_comments(); - cx.notify(); - })) - .on_action(cx.listener(move |this, _: &menu::Confirm, _window, cx| { - this.submit_feedback_message(cx); - })) - .p_2() - .mb_2() - .mx_5() - .gap_1() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().editor_background) - .child(div().w_full().child(editor)) - .child( - h_flex() - .child( - IconButton::new("dismiss-feedback-message", IconName::Close) - .icon_color(Color::Error) - .icon_size(IconSize::XSmall) - .shape(ui::IconButtonShape::Square) - .on_click(cx.listener(move |this, _, _window, cx| { - this.thread_feedback.dismiss_comments(); - cx.notify(); - })), - ) - .child( - IconButton::new("submit-feedback-message", IconName::Return) - .icon_size(IconSize::XSmall) - .shape(ui::IconButtonShape::Square) - .on_click(cx.listener(move |this, _, _window, cx| { - this.submit_feedback_message(cx); - })), - ), - ) - } - - fn handle_feedback_click( - &mut self, - feedback: ThreadFeedback, - window: &mut Window, - cx: &mut Context, - ) { - let Some(thread) = self.thread().cloned() else { - return; - }; - - self.thread_feedback.submit(thread, feedback, window, cx); - cx.notify(); - } - - fn submit_feedback_message(&mut self, cx: &mut Context) { - let Some(thread) = self.thread().cloned() else { - return; - }; - - self.thread_feedback.submit_comments(thread, cx); - cx.notify(); - } - - fn render_vertical_scrollbar(&self, cx: &mut Context) -> Stateful
{ - div() - .id("acp-thread-scrollbar") - .occlude() - .on_mouse_move(cx.listener(|_, _, _, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|_, _, _, cx| { - cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _, cx| { - cx.notify(); - })) - .h_full() - .absolute() - .right_1() - .top_1() - .bottom_0() - .w(px(12.)) - .cursor_default() - .children(Scrollbar::vertical(self.scrollbar_state.clone()).map(|s| s.auto_hide(cx))) - } - - fn render_token_limit_callout( - &self, - line_height: Pixels, - cx: &mut Context, - ) -> Option { - let token_usage = self.thread()?.read(cx).token_usage()?; - let ratio = token_usage.ratio(); - - let (severity, title) = match ratio { - acp_thread::TokenUsageRatio::Normal => return None, - acp_thread::TokenUsageRatio::Warning => { - (Severity::Warning, "Thread reaching the token limit soon") - } - acp_thread::TokenUsageRatio::Exceeded => { - (Severity::Error, "Thread reached the token limit") - } - }; - - let burn_mode_available = self.as_native_thread(cx).is_some_and(|thread| { - thread.read(cx).completion_mode() == CompletionMode::Normal - && thread - .read(cx) - .model() - .is_some_and(|model| model.supports_burn_mode()) - }); - - let description = if burn_mode_available { - "To continue, start a new thread from a summary or turn Burn Mode on." - } else { - "To continue, start a new thread from a summary." - }; - - Some( - Callout::new() - .severity(severity) - .line_height(line_height) - .title(title) - .description(description) - .actions_slot( - h_flex() - .gap_0p5() - .child( - Button::new("start-new-thread", "Start New Thread") - .label_size(LabelSize::Small) - .on_click(cx.listener(|this, _, window, cx| { - let Some(thread) = this.thread() else { - return; - }; - let session_id = thread.read(cx).session_id().clone(); - window.dispatch_action( - crate::NewNativeAgentThreadFromSummary { - from_session_id: session_id, - } - .boxed_clone(), - cx, - ); - })), - ) - .when(burn_mode_available, |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); - })), - ) - }), - ), - ) - } - - fn render_usage_callout(&self, line_height: Pixels, cx: &mut Context) -> Option
{ - if !self.is_using_zed_ai_models(cx) { - return None; - } - - let user_store = self.project.read(cx).user_store().read(cx); - if user_store.is_usage_based_billing_enabled() { - return None; - } - - let plan = user_store.plan().unwrap_or(cloud_llm_client::Plan::ZedFree); - - let usage = user_store.model_request_usage()?; - - Some( - div() - .child(UsageCallout::new(plan, usage)) - .line_height(line_height), - ) - } - - fn settings_changed(&mut self, _window: &mut Window, cx: &mut Context) { - self.entry_view_state.update(cx, |entry_view_state, cx| { - entry_view_state.settings_changed(cx); - }); - } - - pub(crate) fn insert_dragged_files( - &self, - paths: Vec, - added_worktrees: Vec>, - window: &mut Window, - cx: &mut Context, - ) { - self.message_editor.update(cx, |message_editor, cx| { - message_editor.insert_dragged_files(paths, added_worktrees, window, cx); - }) - } - - pub(crate) fn insert_selections(&self, window: &mut Window, cx: &mut Context) { - self.message_editor.update(cx, |message_editor, cx| { - message_editor.insert_selections(window, cx); - }) - } - - fn render_thread_retry_status_callout( - &self, - _window: &mut Window, - _cx: &mut Context, - ) -> Option { - let state = self.thread_retry_status.as_ref()?; - - let next_attempt_in = state - .duration - .saturating_sub(Instant::now().saturating_duration_since(state.started_at)); - if next_attempt_in.is_zero() { - return None; - } - - let next_attempt_in_secs = next_attempt_in.as_secs() + 1; - - let retry_message = if state.max_attempts == 1 { - if next_attempt_in_secs == 1 { - "Retrying. Next attempt in 1 second.".to_string() - } else { - format!("Retrying. Next attempt in {next_attempt_in_secs} seconds.") - } - } else if next_attempt_in_secs == 1 { - format!( - "Retrying. Next attempt in 1 second (Attempt {} of {}).", - state.attempt, state.max_attempts, - ) - } else { - format!( - "Retrying. Next attempt in {next_attempt_in_secs} seconds (Attempt {} of {}).", - state.attempt, state.max_attempts, - ) - }; - - Some( - Callout::new() - .severity(Severity::Warning) - .title(state.last_error.clone()) - .description(retry_message), - ) - } - - fn render_thread_error(&self, window: &mut Window, cx: &mut Context) -> Option
{ - let content = match self.thread_error.as_ref()? { - ThreadError::Other(error) => self.render_any_thread_error(error.clone(), cx), - ThreadError::AuthenticationRequired(error) => { - self.render_authentication_required_error(error.clone(), cx) - } - ThreadError::PaymentRequired => self.render_payment_required_error(cx), - ThreadError::ModelRequestLimitReached(plan) => { - self.render_model_request_limit_reached_error(*plan, cx) - } - ThreadError::ToolUseLimitReached => { - self.render_tool_use_limit_reached_error(window, cx)? - } - }; - - Some(div().child(content)) - } - - fn render_any_thread_error(&self, error: SharedString, cx: &mut Context<'_, Self>) -> Callout { - let can_resume = self - .thread() - .map_or(false, |thread| thread.read(cx).can_resume(cx)); - - let can_enable_burn_mode = self.as_native_thread(cx).map_or(false, |thread| { - let thread = thread.read(cx); - let supports_burn_mode = thread - .model() - .map_or(false, |model| model.supports_burn_mode()); - supports_burn_mode && thread.completion_mode() == CompletionMode::Normal - }); - - Callout::new() - .severity(Severity::Error) - .title("Error") - .icon(IconName::XCircle) - .description(error.clone()) - .actions_slot( - h_flex() - .gap_0p5() - .when(can_resume && can_enable_burn_mode, |this| { - this.child( - Button::new("enable-burn-mode-and-retry", "Enable Burn Mode and Retry") - .icon(IconName::ZedBurnMode) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .label_size(LabelSize::Small) - .on_click(cx.listener(|this, _, window, cx| { - this.toggle_burn_mode(&ToggleBurnMode, window, cx); - this.resume_chat(cx); - })), - ) - }) - .when(can_resume, |this| { - this.child( - Button::new("retry", "Retry") - .icon(IconName::RotateCw) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .label_size(LabelSize::Small) - .on_click(cx.listener(|this, _, _window, cx| { - this.resume_chat(cx); - })), - ) - }) - .child(self.create_copy_button(error.to_string())), - ) - .dismiss_action(self.dismiss_error_button(cx)) - } - - fn render_payment_required_error(&self, cx: &mut Context) -> Callout { - const ERROR_MESSAGE: &str = - "You reached your free usage limit. Upgrade to Zed Pro for more prompts."; - - Callout::new() - .severity(Severity::Error) - .icon(IconName::XCircle) - .title("Free Usage Exceeded") - .description(ERROR_MESSAGE) - .actions_slot( - h_flex() - .gap_0p5() - .child(self.upgrade_button(cx)) - .child(self.create_copy_button(ERROR_MESSAGE)), - ) - .dismiss_action(self.dismiss_error_button(cx)) - } - - fn render_authentication_required_error( - &self, - error: SharedString, - cx: &mut Context, - ) -> Callout { - Callout::new() - .severity(Severity::Error) - .title("Authentication Required") - .icon(IconName::XCircle) - .description(error.clone()) - .actions_slot( - h_flex() - .gap_0p5() - .child(self.authenticate_button(cx)) - .child(self.create_copy_button(error)), - ) - .dismiss_action(self.dismiss_error_button(cx)) - } - - fn render_model_request_limit_reached_error( - &self, - plan: cloud_llm_client::Plan, - cx: &mut Context, - ) -> Callout { - let error_message = match plan { - cloud_llm_client::Plan::ZedPro => "Upgrade to usage-based billing for more prompts.", - cloud_llm_client::Plan::ZedProTrial | cloud_llm_client::Plan::ZedFree => { - "Upgrade to Zed Pro for more prompts." - } - }; - - Callout::new() - .severity(Severity::Error) - .title("Model Prompt Limit Reached") - .icon(IconName::XCircle) - .description(error_message) - .actions_slot( - h_flex() - .gap_0p5() - .child(self.upgrade_button(cx)) - .child(self.create_copy_button(error_message)), - ) - .dismiss_action(self.dismiss_error_button(cx)) - } - - fn render_tool_use_limit_reached_error( - &self, - window: &mut Window, - cx: &mut Context, - ) -> Option { - let thread = self.as_native_thread(cx)?; - let supports_burn_mode = thread - .read(cx) - .model() - .is_some_and(|model| model.supports_burn_mode()); - - let focus_handle = self.focus_handle(cx); - - Some( - Callout::new() - .icon(IconName::Info) - .title("Consecutive tool use limit reached.") - .actions_slot( - h_flex() - .gap_0p5() - .when(supports_burn_mode, |this| { - this.child( - Button::new("continue-burn-mode", "Continue with Burn Mode") - .style(ButtonStyle::Filled) - .style(ButtonStyle::Tinted(ui::TintColor::Accent)) - .layer(ElevationIndex::ModalSurface) - .label_size(LabelSize::Small) - .key_binding( - KeyBinding::for_action_in( - &ContinueWithBurnMode, - &focus_handle, - window, - cx, - ) - .map(|kb| kb.size(rems_from_px(10.))), - ) - .tooltip(Tooltip::text( - "Enable Burn Mode for unlimited tool use.", - )) - .on_click({ - cx.listener(move |this, _, _window, cx| { - thread.update(cx, |thread, cx| { - thread - .set_completion_mode(CompletionMode::Burn, cx); - }); - this.resume_chat(cx); - }) - }), - ) - }) - .child( - Button::new("continue-conversation", "Continue") - .layer(ElevationIndex::ModalSurface) - .label_size(LabelSize::Small) - .key_binding( - KeyBinding::for_action_in( - &ContinueThread, - &focus_handle, - window, - cx, - ) - .map(|kb| kb.size(rems_from_px(10.))), - ) - .on_click(cx.listener(|this, _, _window, cx| { - this.resume_chat(cx); - })), - ), - ), - ) - } - - fn create_copy_button(&self, message: impl Into) -> impl IntoElement { - let message = message.into(); - - IconButton::new("copy", IconName::Copy) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .tooltip(Tooltip::text("Copy Error Message")) - .on_click(move |_, _, cx| { - cx.write_to_clipboard(ClipboardItem::new_string(message.clone())) - }) - } - - fn dismiss_error_button(&self, cx: &mut Context) -> impl IntoElement { - IconButton::new("dismiss", IconName::Close) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .tooltip(Tooltip::text("Dismiss Error")) - .on_click(cx.listener({ - move |this, _, _, cx| { - this.clear_thread_error(cx); - cx.notify(); - } - })) - } - - fn authenticate_button(&self, cx: &mut Context) -> impl IntoElement { - Button::new("authenticate", "Authenticate") - .label_size(LabelSize::Small) - .style(ButtonStyle::Filled) - .on_click(cx.listener({ - move |this, _, window, cx| { - let agent = this.agent.clone(); - let ThreadState::Ready { thread, .. } = &this.thread_state else { - return; - }; - - let connection = thread.read(cx).connection().clone(); - let err = AuthRequired { - description: None, - provider_id: None, - }; - this.clear_thread_error(cx); - let this = cx.weak_entity(); - window.defer(cx, |window, cx| { - Self::handle_auth_required(this, err, agent, connection, window, cx); - }) - } - })) - } - - pub(crate) fn reauthenticate(&mut self, window: &mut Window, cx: &mut Context) { - let agent = self.agent.clone(); - let ThreadState::Ready { thread, .. } = &self.thread_state else { - return; - }; - - let connection = thread.read(cx).connection().clone(); - let err = AuthRequired { - description: None, - provider_id: None, - }; - self.clear_thread_error(cx); - let this = cx.weak_entity(); - window.defer(cx, |window, cx| { - Self::handle_auth_required(this, err, agent, connection, window, cx); - }) - } - - fn upgrade_button(&self, cx: &mut Context) -> impl IntoElement { - Button::new("upgrade", "Upgrade") - .label_size(LabelSize::Small) - .style(ButtonStyle::Tinted(ui::TintColor::Accent)) - .on_click(cx.listener({ - move |this, _, _, cx| { - this.clear_thread_error(cx); - cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx)); - } - })) - } - - fn reset(&mut self, window: &mut Window, cx: &mut Context) { - self.thread_state = Self::initial_state( - self.agent.clone(), - None, - self.workspace.clone(), - self.project.clone(), - window, - cx, - ); - cx.notify(); - } - - pub fn delete_history_entry(&mut self, entry: HistoryEntry, cx: &mut Context) { - let task = match entry { - HistoryEntry::AcpThread(thread) => self.history_store.update(cx, |history, cx| { - history.delete_thread(thread.id.clone(), cx) - }), - HistoryEntry::TextThread(context) => self.history_store.update(cx, |history, cx| { - history.delete_text_thread(context.path.clone(), cx) - }), - }; - task.detach_and_log_err(cx); - } -} - -fn loading_contents_spinner(size: IconSize) -> AnyElement { - Icon::new(IconName::LoadCircle) - .size(size) - .color(Color::Accent) - .with_animation( - "load_context_circle", - Animation::new(Duration::from_secs(3)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), - ) - .into_any_element() -} - -impl Focusable for AcpThreadView { - fn focus_handle(&self, cx: &App) -> FocusHandle { - match self.thread_state { - ThreadState::Loading { .. } | ThreadState::Ready { .. } => { - self.message_editor.focus_handle(cx) - } - ThreadState::LoadError(_) | ThreadState::Unauthenticated { .. } => { - self.focus_handle.clone() - } - } - } -} - -impl Render for AcpThreadView { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let has_messages = self.list_state.item_count() > 0; - let line_height = TextSize::Small.rems(cx).to_pixels(window.rem_size()) * 1.5; - v_flex() .size_full() .key_context("AcpThread") + .on_action(cx.listener(Self::chat)) + .on_action(cx.listener(Self::previous_history_message)) + .on_action(cx.listener(Self::next_history_message)) .on_action(cx.listener(Self::open_agent_diff)) - .on_action(cx.listener(Self::toggle_burn_mode)) - .on_action(cx.listener(Self::keep_all)) - .on_action(cx.listener(Self::reject_all)) - .track_focus(&self.focus_handle) - .bg(cx.theme().colors().panel_background) .child(match &self.thread_state { - ThreadState::Unauthenticated { - connection, - description, - configuration_view, - pending_auth_method, - .. - } => self.render_auth_required_state( - connection, - description.as_ref(), - configuration_view.as_ref(), - pending_auth_method.as_ref(), - window, - cx, - ), - ThreadState::Loading { .. } => v_flex() - .flex_1() - .child(self.render_recent_history(window, cx)), - ThreadState::LoadError(e) => v_flex() - .flex_1() - .size_full() - .items_center() - .justify_end() - .child(self.render_load_error(e, window, cx)), - ThreadState::Ready { .. } => v_flex().flex_1().map(|this| { - if has_messages { - this.child( - list( - self.list_state.clone(), - cx.processor(|this, index: usize, window, cx| { - let Some((entry, len)) = this.thread().and_then(|thread| { - let entries = &thread.read(cx).entries(); - Some((entries.get(index)?, entries.len())) - }) else { - return Empty.into_any(); - }; - this.render_entry(index, len, entry, window, cx) - }), - ) - .with_sizing_behavior(gpui::ListSizingBehavior::Auto) - .flex_grow() - .into_any(), + ThreadState::Unauthenticated { .. } => { + v_flex() + .p_2() + .flex_1() + .items_center() + .justify_center() + .child(self.render_pending_auth_state()) + .child( + h_flex().mt_1p5().justify_center().child( + Button::new("sign-in", format!("Sign in to {}", self.agent.name())) + .on_click(cx.listener(|this, _, window, cx| { + this.authenticate(window, cx) + })), + ), ) - .child(self.render_vertical_scrollbar(cx)) + } + ThreadState::Loading { .. } => v_flex().flex_1().child(self.render_empty_state(cx)), + ThreadState::LoadError(e) => v_flex() + .p_2() + .flex_1() + .items_center() + .justify_center() + .child(self.render_error_state(e, cx)), + ThreadState::Ready { thread, .. } => v_flex().flex_1().map(|this| { + if self.list_state.item_count() > 0 { + this.child( + list(self.list_state.clone()) + .with_sizing_behavior(gpui::ListSizingBehavior::Auto) + .flex_grow() + .into_any(), + ) + .child( + h_flex() + .group("controls") + .mt_1() + .mr_1() + .py_2() + .px(RESPONSE_PADDING_X) + .opacity(0.4) + .hover(|style| style.opacity(1.)) + .flex_wrap() + .justify_end() + .child(open_as_markdown) + .child(scroll_to_top) + .into_any_element(), + ) + .children(match thread.read(cx).status() { + ThreadStatus::Idle | ThreadStatus::WaitingForToolConfirmation => None, + ThreadStatus::Generating => div() + .px_5() + .py_2() + .child(LoadingLabel::new("").size(LabelSize::Small)) + .into(), + }) + .children(self.render_activity_bar(&thread, window, cx)) } else { - this.child(self.render_recent_history(window, cx)) + this.child(self.render_empty_state(cx)) } }), }) - // The activity bar is intentionally rendered outside of the ThreadState::Ready match - // above so that the scrollbar doesn't render behind it. The current setup allows - // the scrollbar to stop exactly at the activity bar start. - .when(has_messages, |this| match &self.thread_state { - ThreadState::Ready { thread, .. } => { - this.children(self.render_activity_bar(thread, window, cx)) - } - _ => this, + .when_some(self.last_error.clone(), |el, error| { + el.child( + div() + .p_2() + .text_xs() + .border_t_1() + .border_color(cx.theme().colors().border) + .bg(cx.theme().status().error_background) + .child( + self.render_markdown(error, default_markdown_style(false, window, cx)), + ), + ) }) - .children(self.render_thread_retry_status_callout(window, cx)) - .children(self.render_thread_error(window, cx)) - .children( - if let Some(usage_callout) = self.render_usage_callout(line_height, cx) { - Some(usage_callout.into_any_element()) - } else { - self.render_token_limit_callout(line_height, cx) - .map(|token_limit_callout| token_limit_callout.into_any_element()) - }, - ) .child(self.render_message_editor(window, cx)) } } -fn default_markdown_style( - buffer_font: bool, - muted_text: bool, - window: &Window, - cx: &App, -) -> MarkdownStyle { +fn user_message_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { + let mut style = default_markdown_style(false, window, cx); + let mut text_style = window.text_style(); + let theme_settings = ThemeSettings::get_global(cx); + + let buffer_font = theme_settings.buffer_font.family.clone(); + let buffer_font_size = TextSize::Small.rems(cx); + + text_style.refine(&TextStyleRefinement { + font_family: Some(buffer_font), + font_size: Some(buffer_font_size.into()), + ..Default::default() + }); + + style.base_text_style = text_style; + style.link_callback = Some(Rc::new(move |url, cx| { + if MentionPath::try_parse(url).is_some() { + let colors = cx.theme().colors(); + Some(TextStyleRefinement { + background_color: Some(colors.element_background), + ..Default::default() + }) + } else { + None + } + })); + style +} + +fn default_markdown_style(buffer_font: bool, window: &Window, cx: &App) -> MarkdownStyle { let theme_settings = ThemeSettings::get_global(cx); let colors = cx.theme().colors(); @@ -5041,26 +2324,20 @@ fn default_markdown_style( TextSize::Default.rems(cx) }; - let text_color = if muted_text { - colors.text_muted - } else { - colors.text - }; - text_style.refine(&TextStyleRefinement { font_family: Some(font_family), font_fallbacks: theme_settings.ui_font.fallbacks.clone(), font_features: Some(theme_settings.ui_font.features.clone()), font_size: Some(font_size.into()), line_height: Some(line_height.into()), - color: Some(text_color), + color: Some(cx.theme().colors().text), ..Default::default() }); MarkdownStyle { base_text_style: text_style.clone(), syntax: cx.theme().syntax().clone(), - selection_background_color: colors.element_selection_background, + selection_background_color: cx.theme().colors().element_selection_background, code_block_overflow_x_scroll: true, table_overflow_x_scroll: true, heading_level_styles: Some(HeadingLevelStyles { @@ -5146,7 +2423,7 @@ fn plan_label_markdown_style( window: &Window, cx: &App, ) -> MarkdownStyle { - let default_md_style = default_markdown_style(false, false, window, cx); + let default_md_style = default_markdown_style(false, window, cx); MarkdownStyle { base_text_style: TextStyle { @@ -5164,913 +2441,3 @@ fn plan_label_markdown_style( ..default_md_style } } - -fn terminal_command_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { - let default_md_style = default_markdown_style(true, false, window, cx); - - MarkdownStyle { - base_text_style: TextStyle { - ..default_md_style.base_text_style - }, - selection_background_color: cx.theme().colors().element_selection_background, - ..Default::default() - } -} - -#[cfg(test)] -pub(crate) mod tests { - use acp_thread::StubAgentConnection; - use agent_client_protocol::SessionId; - use assistant_context::ContextStore; - use editor::EditorSettings; - use fs::FakeFs; - use gpui::{EventEmitter, SemanticVersion, TestAppContext, VisualTestContext}; - use project::Project; - use serde_json::json; - use settings::SettingsStore; - use std::any::Any; - use std::path::Path; - use workspace::Item; - - use super::*; - - #[gpui::test] - async fn test_drop(cx: &mut TestAppContext) { - init_test(cx); - - let (thread_view, _cx) = setup_thread_view(StubAgentServer::default_response(), cx).await; - let weak_view = thread_view.downgrade(); - drop(thread_view); - assert!(!weak_view.is_upgradable()); - } - - #[gpui::test] - async fn test_notification_for_stop_event(cx: &mut TestAppContext) { - init_test(cx); - - let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await; - - let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); - message_editor.update_in(cx, |editor, window, cx| { - editor.set_text("Hello", window, cx); - }); - - cx.deactivate_window(); - - thread_view.update_in(cx, |thread_view, window, cx| { - thread_view.send(window, cx); - }); - - cx.run_until_parked(); - - assert!( - cx.windows() - .iter() - .any(|window| window.downcast::().is_some()) - ); - } - - #[gpui::test] - async fn test_notification_for_error(cx: &mut TestAppContext) { - init_test(cx); - - let (thread_view, cx) = - setup_thread_view(StubAgentServer::new(SaboteurAgentConnection), cx).await; - - let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); - message_editor.update_in(cx, |editor, window, cx| { - editor.set_text("Hello", window, cx); - }); - - cx.deactivate_window(); - - thread_view.update_in(cx, |thread_view, window, cx| { - thread_view.send(window, cx); - }); - - cx.run_until_parked(); - - assert!( - cx.windows() - .iter() - .any(|window| window.downcast::().is_some()) - ); - } - - #[gpui::test] - async fn test_notification_for_tool_authorization(cx: &mut TestAppContext) { - init_test(cx); - - let tool_call_id = acp::ToolCallId("1".into()); - let tool_call = acp::ToolCall { - id: tool_call_id.clone(), - title: "Label".into(), - kind: acp::ToolKind::Edit, - status: acp::ToolCallStatus::Pending, - content: vec!["hi".into()], - locations: vec![], - raw_input: None, - raw_output: None, - }; - let connection = - StubAgentConnection::new().with_permission_requests(HashMap::from_iter([( - tool_call_id, - vec![acp::PermissionOption { - id: acp::PermissionOptionId("1".into()), - name: "Allow".into(), - kind: acp::PermissionOptionKind::AllowOnce, - }], - )])); - - connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]); - - let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await; - - let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); - message_editor.update_in(cx, |editor, window, cx| { - editor.set_text("Hello", window, cx); - }); - - cx.deactivate_window(); - - thread_view.update_in(cx, |thread_view, window, cx| { - thread_view.send(window, cx); - }); - - cx.run_until_parked(); - - assert!( - cx.windows() - .iter() - .any(|window| window.downcast::().is_some()) - ); - } - - async fn setup_thread_view( - agent: impl AgentServer + 'static, - cx: &mut TestAppContext, - ) -> (Entity, &mut VisualTestContext) { - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, [], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - let context_store = - cx.update(|_window, cx| cx.new(|cx| ContextStore::fake(project.clone(), cx))); - let history_store = - cx.update(|_window, cx| cx.new(|cx| HistoryStore::new(context_store, cx))); - - let thread_view = cx.update(|window, cx| { - cx.new(|cx| { - AcpThreadView::new( - Rc::new(agent), - None, - None, - workspace.downgrade(), - project, - history_store, - None, - window, - cx, - ) - }) - }); - cx.run_until_parked(); - (thread_view, cx) - } - - fn add_to_workspace(thread_view: Entity, cx: &mut VisualTestContext) { - let workspace = thread_view.read_with(cx, |thread_view, _cx| thread_view.workspace.clone()); - - workspace - .update_in(cx, |workspace, window, cx| { - workspace.add_item_to_active_pane( - Box::new(cx.new(|_| ThreadViewItem(thread_view.clone()))), - None, - true, - window, - cx, - ); - }) - .unwrap(); - } - - struct ThreadViewItem(Entity); - - impl Item for ThreadViewItem { - type Event = (); - - fn include_in_nav_history() -> bool { - false - } - - fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { - "Test".into() - } - } - - impl EventEmitter<()> for ThreadViewItem {} - - impl Focusable for ThreadViewItem { - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.0.read(cx).focus_handle(cx) - } - } - - impl Render for ThreadViewItem { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - self.0.clone().into_any_element() - } - } - - struct StubAgentServer { - connection: C, - } - - impl StubAgentServer { - fn new(connection: C) -> Self { - Self { connection } - } - } - - impl StubAgentServer { - fn default_response() -> Self { - let conn = StubAgentConnection::new(); - conn.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk { - content: "Default response".into(), - }]); - Self::new(conn) - } - } - - impl AgentServer for StubAgentServer - where - C: 'static + AgentConnection + Send + Clone, - { - fn telemetry_id(&self) -> &'static str { - "test" - } - - fn logo(&self) -> ui::IconName { - ui::IconName::Ai - } - - fn name(&self) -> SharedString { - "Test".into() - } - - fn empty_state_headline(&self) -> SharedString { - "Test".into() - } - - fn empty_state_message(&self) -> SharedString { - "Test".into() - } - - fn connect( - &self, - _root_dir: &Path, - _project: &Entity, - _cx: &mut App, - ) -> Task>> { - Task::ready(Ok(Rc::new(self.connection.clone()))) - } - - fn into_any(self: Rc) -> Rc { - self - } - } - - #[derive(Clone)] - struct SaboteurAgentConnection; - - impl AgentConnection for SaboteurAgentConnection { - fn new_thread( - self: Rc, - project: Entity, - _cwd: &Path, - cx: &mut gpui::App, - ) -> Task>> { - Task::ready(Ok(cx.new(|cx| { - let action_log = cx.new(|_| ActionLog::new(project.clone())); - AcpThread::new( - "SaboteurAgentConnection", - self, - project, - action_log, - SessionId("test".into()), - watch::Receiver::constant(acp::PromptCapabilities { - image: true, - audio: true, - embedded_context: true, - }), - cx, - ) - }))) - } - - fn auth_methods(&self) -> &[acp::AuthMethod] { - &[] - } - - fn authenticate( - &self, - _method_id: acp::AuthMethodId, - _cx: &mut App, - ) -> Task> { - unimplemented!() - } - - fn prompt( - &self, - _id: Option, - _params: acp::PromptRequest, - _cx: &mut App, - ) -> Task> { - Task::ready(Err(anyhow::anyhow!("Error prompting"))) - } - - fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) { - unimplemented!() - } - - fn into_any(self: Rc) -> Rc { - self - } - } - - pub(crate) 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); - prompt_store::init(cx) - }); - } - - #[gpui::test] - async fn test_rewind_views(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/project", - json!({ - "test1.txt": "old content 1", - "test2.txt": "old content 2" - }), - ) - .await; - let project = Project::test(fs, [Path::new("/project")], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - let context_store = - cx.update(|_window, cx| cx.new(|cx| ContextStore::fake(project.clone(), cx))); - let history_store = - cx.update(|_window, cx| cx.new(|cx| HistoryStore::new(context_store, cx))); - - let connection = Rc::new(StubAgentConnection::new()); - let thread_view = cx.update(|window, cx| { - cx.new(|cx| { - AcpThreadView::new( - Rc::new(StubAgentServer::new(connection.as_ref().clone())), - None, - None, - workspace.downgrade(), - project.clone(), - history_store.clone(), - None, - window, - cx, - ) - }) - }); - - cx.run_until_parked(); - - let thread = thread_view - .read_with(cx, |view, _| view.thread().cloned()) - .unwrap(); - - // First user message - connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(acp::ToolCall { - id: acp::ToolCallId("tool1".into()), - title: "Edit file 1".into(), - kind: acp::ToolKind::Edit, - status: acp::ToolCallStatus::Completed, - content: vec![acp::ToolCallContent::Diff { - diff: acp::Diff { - path: "/project/test1.txt".into(), - old_text: Some("old content 1".into()), - new_text: "new content 1".into(), - }, - }], - locations: vec![], - raw_input: None, - raw_output: None, - })]); - - thread - .update(cx, |thread, cx| thread.send_raw("Give me a diff", cx)) - .await - .unwrap(); - cx.run_until_parked(); - - thread.read_with(cx, |thread, _| { - assert_eq!(thread.entries().len(), 2); - }); - - thread_view.read_with(cx, |view, cx| { - view.entry_view_state.read_with(cx, |entry_view_state, _| { - assert!( - entry_view_state - .entry(0) - .unwrap() - .message_editor() - .is_some() - ); - assert!(entry_view_state.entry(1).unwrap().has_content()); - }); - }); - - // Second user message - connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(acp::ToolCall { - id: acp::ToolCallId("tool2".into()), - title: "Edit file 2".into(), - kind: acp::ToolKind::Edit, - status: acp::ToolCallStatus::Completed, - content: vec![acp::ToolCallContent::Diff { - diff: acp::Diff { - path: "/project/test2.txt".into(), - old_text: Some("old content 2".into()), - new_text: "new content 2".into(), - }, - }], - locations: vec![], - raw_input: None, - raw_output: None, - })]); - - thread - .update(cx, |thread, cx| thread.send_raw("Another one", cx)) - .await - .unwrap(); - cx.run_until_parked(); - - let second_user_message_id = thread.read_with(cx, |thread, _| { - assert_eq!(thread.entries().len(), 4); - let AgentThreadEntry::UserMessage(user_message) = &thread.entries()[2] else { - panic!(); - }; - user_message.id.clone().unwrap() - }); - - thread_view.read_with(cx, |view, cx| { - view.entry_view_state.read_with(cx, |entry_view_state, _| { - assert!( - entry_view_state - .entry(0) - .unwrap() - .message_editor() - .is_some() - ); - assert!(entry_view_state.entry(1).unwrap().has_content()); - assert!( - entry_view_state - .entry(2) - .unwrap() - .message_editor() - .is_some() - ); - assert!(entry_view_state.entry(3).unwrap().has_content()); - }); - }); - - // Rewind to first message - thread - .update(cx, |thread, cx| thread.rewind(second_user_message_id, cx)) - .await - .unwrap(); - - cx.run_until_parked(); - - thread.read_with(cx, |thread, _| { - assert_eq!(thread.entries().len(), 2); - }); - - thread_view.read_with(cx, |view, cx| { - view.entry_view_state.read_with(cx, |entry_view_state, _| { - assert!( - entry_view_state - .entry(0) - .unwrap() - .message_editor() - .is_some() - ); - assert!(entry_view_state.entry(1).unwrap().has_content()); - - // Old views should be dropped - assert!(entry_view_state.entry(2).is_none()); - assert!(entry_view_state.entry(3).is_none()); - }); - }); - } - - #[gpui::test] - async fn test_message_editing_cancel(cx: &mut TestAppContext) { - init_test(cx); - - let connection = StubAgentConnection::new(); - - connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk { - content: acp::ContentBlock::Text(acp::TextContent { - text: "Response".into(), - annotations: None, - }), - }]); - - let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await; - add_to_workspace(thread_view.clone(), cx); - - let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); - message_editor.update_in(cx, |editor, window, cx| { - editor.set_text("Original message to edit", window, cx); - }); - thread_view.update_in(cx, |thread_view, window, cx| { - thread_view.send(window, cx); - }); - - cx.run_until_parked(); - - let user_message_editor = thread_view.read_with(cx, |view, cx| { - assert_eq!(view.editing_message, None); - - view.entry_view_state - .read(cx) - .entry(0) - .unwrap() - .message_editor() - .unwrap() - .clone() - }); - - // Focus - cx.focus(&user_message_editor); - thread_view.read_with(cx, |view, _cx| { - assert_eq!(view.editing_message, Some(0)); - }); - - // Edit - user_message_editor.update_in(cx, |editor, window, cx| { - editor.set_text("Edited message content", window, cx); - }); - - // Cancel - user_message_editor.update_in(cx, |_editor, window, cx| { - window.dispatch_action(Box::new(editor::actions::Cancel), cx); - }); - - thread_view.read_with(cx, |view, _cx| { - assert_eq!(view.editing_message, None); - }); - - user_message_editor.read_with(cx, |editor, cx| { - assert_eq!(editor.text(cx), "Original message to edit"); - }); - } - - #[gpui::test] - async fn test_message_doesnt_send_if_empty(cx: &mut TestAppContext) { - init_test(cx); - - let connection = StubAgentConnection::new(); - - let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await; - add_to_workspace(thread_view.clone(), cx); - - let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); - let mut events = cx.events(&message_editor); - message_editor.update_in(cx, |editor, window, cx| { - editor.set_text("", window, cx); - }); - - message_editor.update_in(cx, |_editor, window, cx| { - window.dispatch_action(Box::new(Chat), cx); - }); - cx.run_until_parked(); - // We shouldn't have received any messages - assert!(matches!( - events.try_next(), - Err(futures::channel::mpsc::TryRecvError { .. }) - )); - } - - #[gpui::test] - async fn test_message_editing_regenerate(cx: &mut TestAppContext) { - init_test(cx); - - let connection = StubAgentConnection::new(); - - connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk { - content: acp::ContentBlock::Text(acp::TextContent { - text: "Response".into(), - annotations: None, - }), - }]); - - let (thread_view, cx) = - setup_thread_view(StubAgentServer::new(connection.clone()), cx).await; - add_to_workspace(thread_view.clone(), cx); - - let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); - message_editor.update_in(cx, |editor, window, cx| { - editor.set_text("Original message to edit", window, cx); - }); - thread_view.update_in(cx, |thread_view, window, cx| { - thread_view.send(window, cx); - }); - - cx.run_until_parked(); - - let user_message_editor = thread_view.read_with(cx, |view, cx| { - assert_eq!(view.editing_message, None); - assert_eq!(view.thread().unwrap().read(cx).entries().len(), 2); - - view.entry_view_state - .read(cx) - .entry(0) - .unwrap() - .message_editor() - .unwrap() - .clone() - }); - - // Focus - cx.focus(&user_message_editor); - - // Edit - user_message_editor.update_in(cx, |editor, window, cx| { - editor.set_text("Edited message content", window, cx); - }); - - // Send - connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk { - content: acp::ContentBlock::Text(acp::TextContent { - text: "New Response".into(), - annotations: None, - }), - }]); - - user_message_editor.update_in(cx, |_editor, window, cx| { - window.dispatch_action(Box::new(Chat), cx); - }); - - cx.run_until_parked(); - - thread_view.read_with(cx, |view, cx| { - assert_eq!(view.editing_message, None); - - let entries = view.thread().unwrap().read(cx).entries(); - assert_eq!(entries.len(), 2); - assert_eq!( - entries[0].to_markdown(cx), - "## User\n\nEdited message content\n\n" - ); - assert_eq!( - entries[1].to_markdown(cx), - "## Assistant\n\nNew Response\n\n" - ); - - let new_editor = view.entry_view_state.read_with(cx, |state, _cx| { - assert!(!state.entry(1).unwrap().has_content()); - state.entry(0).unwrap().message_editor().unwrap().clone() - }); - - assert_eq!(new_editor.read(cx).text(cx), "Edited message content"); - }) - } - - #[gpui::test] - async fn test_message_editing_while_generating(cx: &mut TestAppContext) { - init_test(cx); - - let connection = StubAgentConnection::new(); - - let (thread_view, cx) = - setup_thread_view(StubAgentServer::new(connection.clone()), cx).await; - add_to_workspace(thread_view.clone(), cx); - - let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); - message_editor.update_in(cx, |editor, window, cx| { - editor.set_text("Original message to edit", window, cx); - }); - thread_view.update_in(cx, |thread_view, window, cx| { - thread_view.send(window, cx); - }); - - cx.run_until_parked(); - - let (user_message_editor, session_id) = thread_view.read_with(cx, |view, cx| { - let thread = view.thread().unwrap().read(cx); - assert_eq!(thread.entries().len(), 1); - - let editor = view - .entry_view_state - .read(cx) - .entry(0) - .unwrap() - .message_editor() - .unwrap() - .clone(); - - (editor, thread.session_id().clone()) - }); - - // Focus - cx.focus(&user_message_editor); - - thread_view.read_with(cx, |view, _cx| { - assert_eq!(view.editing_message, Some(0)); - }); - - // Edit - user_message_editor.update_in(cx, |editor, window, cx| { - editor.set_text("Edited message content", window, cx); - }); - - thread_view.read_with(cx, |view, _cx| { - assert_eq!(view.editing_message, Some(0)); - }); - - // Finish streaming response - cx.update(|_, cx| { - connection.send_update( - session_id.clone(), - acp::SessionUpdate::AgentMessageChunk { - content: acp::ContentBlock::Text(acp::TextContent { - text: "Response".into(), - annotations: None, - }), - }, - cx, - ); - connection.end_turn(session_id, acp::StopReason::EndTurn); - }); - - thread_view.read_with(cx, |view, _cx| { - assert_eq!(view.editing_message, Some(0)); - }); - - cx.run_until_parked(); - - // Should still be editing - cx.update(|window, cx| { - assert!(user_message_editor.focus_handle(cx).is_focused(window)); - assert_eq!(thread_view.read(cx).editing_message, Some(0)); - assert_eq!( - user_message_editor.read(cx).text(cx), - "Edited message content" - ); - }); - } - - #[gpui::test] - async fn test_interrupt(cx: &mut TestAppContext) { - init_test(cx); - - let connection = StubAgentConnection::new(); - - let (thread_view, cx) = - setup_thread_view(StubAgentServer::new(connection.clone()), cx).await; - add_to_workspace(thread_view.clone(), cx); - - let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); - message_editor.update_in(cx, |editor, window, cx| { - editor.set_text("Message 1", window, cx); - }); - thread_view.update_in(cx, |thread_view, window, cx| { - thread_view.send(window, cx); - }); - - let (thread, session_id) = thread_view.read_with(cx, |view, cx| { - let thread = view.thread().unwrap(); - - (thread.clone(), thread.read(cx).session_id().clone()) - }); - - cx.run_until_parked(); - - cx.update(|_, cx| { - connection.send_update( - session_id.clone(), - acp::SessionUpdate::AgentMessageChunk { - content: "Message 1 resp".into(), - }, - cx, - ); - }); - - cx.run_until_parked(); - - thread.read_with(cx, |thread, cx| { - assert_eq!( - thread.to_markdown(cx), - indoc::indoc! {" - ## User - - Message 1 - - ## Assistant - - Message 1 resp - - "} - ) - }); - - message_editor.update_in(cx, |editor, window, cx| { - editor.set_text("Message 2", window, cx); - }); - thread_view.update_in(cx, |thread_view, window, cx| { - thread_view.send(window, cx); - }); - - cx.update(|_, cx| { - // Simulate a response sent after beginning to cancel - connection.send_update( - session_id.clone(), - acp::SessionUpdate::AgentMessageChunk { - content: "onse".into(), - }, - cx, - ); - }); - - cx.run_until_parked(); - - // Last Message 1 response should appear before Message 2 - thread.read_with(cx, |thread, cx| { - assert_eq!( - thread.to_markdown(cx), - indoc::indoc! {" - ## User - - Message 1 - - ## Assistant - - Message 1 response - - ## User - - Message 2 - - "} - ) - }); - - cx.update(|_, cx| { - connection.send_update( - session_id.clone(), - acp::SessionUpdate::AgentMessageChunk { - content: "Message 2 response".into(), - }, - cx, - ); - connection.end_turn(session_id.clone(), acp::StopReason::EndTurn); - }); - - cx.run_until_parked(); - - thread.read_with(cx, |thread, cx| { - assert_eq!( - thread.to_markdown(cx), - indoc::indoc! {" - ## User - - Message 1 - - ## Assistant - - Message 1 response - - ## User - - Message 2 - - ## Assistant - - Message 2 response - - "} - ) - }); - } -} diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index e0cecad6e2..1669c24a1b 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -69,6 +69,8 @@ pub struct ActiveThread { messages: Vec, list_state: ListState, scrollbar_state: ScrollbarState, + show_scrollbar: bool, + hide_scrollbar_task: Option>, rendered_messages_by_id: HashMap, rendered_tool_uses: HashMap, editing_message: Option<(MessageId, EditingMessageState)>, @@ -434,7 +436,7 @@ fn render_markdown_code_block( .child(content) .child( Icon::new(IconName::ArrowUpRight) - .size(IconSize::Small) + .size(IconSize::XSmall) .color(Color::Ignored), ), ) @@ -491,7 +493,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; + let code_block_range = metadata.content_range.clone(); move |_event, _window, cx| { active_thread.update(cx, |this, cx| { this.copied_code_block_ids.insert((message_id, ix)); @@ -532,6 +534,7 @@ 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); @@ -777,14 +780,22 @@ impl ActiveThread { cx.observe_global::(|_, cx| cx.notify()), ]; - let list_state = ListState::new(0, ListAlignment::Bottom, px(2048.)); - - let workspace_subscription = workspace.upgrade().map(|workspace| { - cx.observe_release(&workspace, |this, _, cx| { - this.dismiss_notifications(cx); - }) + let list_state = ListState::new(0, ListAlignment::Bottom, px(2048.), { + let this = cx.entity().downgrade(); + move |ix, window: &mut Window, cx: &mut App| { + this.update(cx, |this, cx| this.render_message(ix, window, cx)) + .unwrap() + } }); + let workspace_subscription = if let Some(workspace) = workspace.upgrade() { + Some(cx.observe_release(&workspace, |this, _, cx| { + this.dismiss_notifications(cx); + })) + } else { + None + }; + let mut this = Self { language_registry, thread_store, @@ -800,7 +811,9 @@ impl ActiveThread { expanded_thinking_segments: HashMap::default(), expanded_code_blocks: HashMap::default(), list_state: list_state.clone(), - scrollbar_state: ScrollbarState::new(list_state).parent_entity(&cx.entity()), + scrollbar_state: ScrollbarState::new(list_state), + show_scrollbar: false, + hide_scrollbar_task: None, editing_message: None, last_error: None, copied_code_block_ids: HashSet::default(), @@ -913,7 +926,7 @@ impl ActiveThread { ) { let rendered = self .rendered_tool_uses - .entry(tool_use_id) + .entry(tool_use_id.clone()) .or_insert_with(|| RenderedToolUse { label: cx.new(|cx| { Markdown::new("".into(), Some(self.language_registry.clone()), None, cx) @@ -1041,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); } } @@ -1069,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) - && let Some(rendered_message) = self.thread.update(cx, |thread, cx| { + if let Some(index) = self.messages.iter().position(|id| id == message_id) { + if 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(), @@ -1081,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) => { @@ -1215,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, window, primary, cx); + self.pop_up(icon, caption.into(), title.clone(), window, primary, cx); } } NotifyWhenAgentWaiting::AllScreens => { @@ -1269,61 +1282,62 @@ impl ActiveThread { }) }) .log_err() - && 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); + 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(); + 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 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::(window, cx); - }); - } - }) - .log_err(); - }); + if let Some(workspace) = workspace_handle.upgrade() { + workspace.update(_cx, |workspace, cx| { + workspace.focus_panel::(window, cx); + }); + } + }) + .log_err(); + }); - this.dismiss_notifications(cx); + this.dismiss_notifications(cx); + } + AgentNotificationEvent::Dismissed => { + this.dismiss_notifications(cx); + } } - AgentNotificationEvent::Dismissed => { - this.dismiss_notifications(cx); - } - } - })); + })); - self.notifications.push(screen_window); + 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(); + // 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); - }); - } - }) - }); + 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); + }); + } + } + }) + }); + } } } @@ -1370,12 +1384,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: &EditorEvent, cx| { - if event == &EditorEvent::BufferEdited { - this.update_editing_message_token_count(true, cx); - } - }); + let buffer_edited_subscription = cx.subscribe(&editor, |this, _, event, cx| match event { + EditorEvent::BufferEdited => { + this.update_editing_message_token_count(true, cx); + } + _ => {} + }); let context_picker_menu_handle = PopoverMenuHandle::default(); let context_strip = cx.new(|cx| { @@ -1595,6 +1609,11 @@ 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); @@ -1757,7 +1776,7 @@ impl ActiveThread { .thread .read(cx) .message(message_id) - .map(|msg| msg.to_message_content()) + .map(|msg| msg.to_string()) .unwrap_or_default(); telemetry::event!( @@ -1827,12 +1846,7 @@ impl ActiveThread { ))) } - fn render_message( - &mut self, - ix: usize, - window: &mut Window, - cx: &mut Context, - ) -> AnyElement { + fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context) -> AnyElement { let message_id = self.messages[ix]; let workspace = self.workspace.clone(); let thread = self.thread.read(cx); @@ -1887,9 +1901,8 @@ impl ActiveThread { (colors.editor_background, colors.panel_background) }; - let open_as_markdown = IconButton::new(("open-as-markdown", ix), IconName::FileMarkdown) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::Small) + let open_as_markdown = IconButton::new(("open-as-markdown", ix), IconName::DocumentText) + .icon_size(IconSize::XSmall) .icon_color(Color::Ignored) .tooltip(Tooltip::text("Open Thread as Markdown")) .on_click({ @@ -1903,9 +1916,8 @@ impl ActiveThread { } }); - let scroll_to_top = IconButton::new(("scroll_to_top", ix), IconName::ArrowUp) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::Small) + let scroll_to_top = IconButton::new(("scroll_to_top", ix), IconName::ArrowUpAlt) + .icon_size(IconSize::XSmall) .icon_color(Color::Ignored) .tooltip(Tooltip::text("Scroll To Top")) .on_click(cx.listener(move |this, _, _, cx| { @@ -1919,7 +1931,6 @@ impl ActiveThread { .py_2() .px(RESPONSE_PADDING_X) .mr_1() - .gap_1() .opacity(0.4) .hover(|style| style.opacity(1.)) .gap_1p5() @@ -1943,8 +1954,7 @@ impl ActiveThread { h_flex() .child( IconButton::new(("feedback-thumbs-up", ix), IconName::ThumbsUp) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::Small) + .icon_size(IconSize::XSmall) .icon_color(match feedback { ThreadFeedback::Positive => Color::Accent, ThreadFeedback::Negative => Color::Ignored, @@ -1961,8 +1971,7 @@ impl ActiveThread { ) .child( IconButton::new(("feedback-thumbs-down", ix), IconName::ThumbsDown) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::Small) + .icon_size(IconSize::XSmall) .icon_color(match feedback { ThreadFeedback::Positive => Color::Ignored, ThreadFeedback::Negative => Color::Accent, @@ -1995,8 +2004,7 @@ impl ActiveThread { h_flex() .child( IconButton::new(("feedback-thumbs-up", ix), IconName::ThumbsUp) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::Small) + .icon_size(IconSize::XSmall) .icon_color(Color::Ignored) .tooltip(Tooltip::text("Helpful Response")) .on_click(cx.listener(move |this, _, window, cx| { @@ -2010,8 +2018,7 @@ impl ActiveThread { ) .child( IconButton::new(("feedback-thumbs-down", ix), IconName::ThumbsDown) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::Small) + .icon_size(IconSize::XSmall) .icon_color(Color::Ignored) .tooltip(Tooltip::text("Not Helpful")) .on_click(cx.listener(move |this, _, window, cx| { @@ -2104,7 +2111,7 @@ impl ActiveThread { .gap_1() .children(message_content) .when_some(editing_message_state, |this, state| { - let focus_handle = state.editor.focus_handle(cx); + let focus_handle = state.editor.focus_handle(cx).clone(); this.child( h_flex() @@ -2165,6 +2172,7 @@ 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", @@ -2237,7 +2245,9 @@ impl ActiveThread { let after_editing_message = self .editing_message .as_ref() - .is_some_and(|(editing_message_id, _)| message_id > *editing_message_id); + .map_or(false, |(editing_message_id, _)| { + message_id > *editing_message_id + }); let backdrop = div() .id(("backdrop", ix)) @@ -2257,12 +2267,13 @@ 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 { - match last_restore_checkpoint { - LastRestoreCheckpoint::Pending { .. } => is_pending = true, - LastRestoreCheckpoint::Error { error: err, .. } => { - error = Some(err.clone()); + 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()); + } } } } @@ -2303,7 +2314,7 @@ impl ActiveThread { .into_any_element() } else if let Some(error) = error { restore_checkpoint_button - .tooltip(Tooltip::text(error)) + .tooltip(Tooltip::text(error.to_string())) .into_any_element() } else { restore_checkpoint_button.into_any_element() @@ -2344,6 +2355,7 @@ 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() @@ -2459,7 +2471,7 @@ impl ActiveThread { message_id, index, content.clone(), - scroll_handle, + &scroll_handle, Some(index) == pending_thinking_segment_index, window, cx, @@ -2583,7 +2595,7 @@ impl ActiveThread { .id(("message-container", ix)) .py_1() .px_2p5() - .child(Banner::new().severity(Severity::Warning).child(message)) + .child(Banner::new().severity(ui::Severity::Warning).child(message)) } fn render_message_thinking_segment( @@ -2617,7 +2629,7 @@ impl ActiveThread { h_flex() .gap_1p5() .child( - Icon::new(IconName::ToolThink) + Icon::new(IconName::ToolBulb) .size(IconSize::Small) .color(Color::Muted), ) @@ -2743,7 +2755,7 @@ impl ActiveThread { h_flex() .gap_1p5() .child( - Icon::new(IconName::ToolThink) + Icon::new(IconName::LightBulb) .size(IconSize::XSmall) .color(Color::Muted), ) @@ -3355,7 +3367,7 @@ impl ActiveThread { .mr_0p5(), ) .child( - IconButton::new("open-prompt-library", IconName::ArrowUpRight) + IconButton::new("open-prompt-library", IconName::ArrowUpRightAlt) .shape(ui::IconButtonShape::Square) .icon_size(IconSize::XSmall) .icon_color(Color::Ignored) @@ -3390,7 +3402,7 @@ impl ActiveThread { .mr_0p5(), ) .child( - IconButton::new("open-rule", IconName::ArrowUpRight) + IconButton::new("open-rule", IconName::ArrowUpRightAlt) .shape(ui::IconButtonShape::Square) .icon_size(IconSize::XSmall) .icon_color(Color::Ignored) @@ -3491,37 +3503,60 @@ impl ActiveThread { } } - fn render_vertical_scrollbar(&self, cx: &mut Context) -> Stateful
{ - div() - .occlude() - .id("active-thread-scrollbar") - .on_mouse_move(cx.listener(|_, _, _, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|_, _, _, cx| { + fn render_vertical_scrollbar(&self, cx: &mut Context) -> Option> { + if !self.show_scrollbar && !self.scrollbar_state.is_dragging() { + return None; + } + + Some( + div() + .occlude() + .id("active-thread-scrollbar") + .on_mouse_move(cx.listener(|_, _, _, cx| { + cx.notify(); + cx.stop_propagation() + })) + .on_hover(|_, _, cx| { cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _, cx| { - cx.notify(); - })) - .h_full() - .absolute() - .right_1() - .top_1() - .bottom_0() - .w(px(12.)) - .cursor_default() - .children(Scrollbar::vertical(self.scrollbar_state.clone()).map(|s| s.auto_hide(cx))) + }) + .on_any_mouse_down(|_, _, cx| { + cx.stop_propagation(); + }) + .on_mouse_up( + MouseButton::Left, + cx.listener(|_, _, _, cx| { + cx.stop_propagation(); + }), + ) + .on_scroll_wheel(cx.listener(|_, _, _, cx| { + cx.notify(); + })) + .h_full() + .absolute() + .right_1() + .top_1() + .bottom_0() + .w(px(12.)) + .cursor_default() + .children(Scrollbar::vertical(self.scrollbar_state.clone())), + ) + } + + fn hide_scrollbar_later(&mut self, cx: &mut Context) { + const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1); + self.hide_scrollbar_task = Some(cx.spawn(async move |thread, cx| { + cx.background_executor() + .timer(SCROLLBAR_SHOW_INTERVAL) + .await; + thread + .update(cx, |thread, cx| { + if !thread.scrollbar_state.is_dragging() { + thread.show_scrollbar = false; + cx.notify(); + } + }) + .log_err(); + })) } pub fn is_codeblock_expanded(&self, message_id: MessageId, ix: usize) -> bool { @@ -3562,8 +3597,26 @@ impl Render for ActiveThread { .size_full() .relative() .bg(cx.theme().colors().panel_background) - .child(list(self.list_state.clone(), cx.processor(Self::render_message)).flex_grow()) - .child(self.render_vertical_scrollbar(cx)) + .on_mouse_move(cx.listener(|this, _, _, cx| { + this.show_scrollbar = true; + this.hide_scrollbar_later(cx); + cx.notify(); + })) + .on_scroll_wheel(cx.listener(|this, _, _, cx| { + this.show_scrollbar = true; + this.hide_scrollbar_later(cx); + cx.notify(); + })) + .on_mouse_up( + MouseButton::Left, + cx.listener(|this, _, _, cx| { + this.hide_scrollbar_later(cx); + }), + ) + .child(list(self.list_state.clone()).flex_grow()) + .when_some(self.render_vertical_scrollbar(cx), |this, scrollbar| { + this.child(scrollbar) + }) } } @@ -3767,6 +3820,7 @@ mod tests { use super::*; use agent::{MessageSegment, context::ContextLoadResult, thread_store}; use assistant_tool::{ToolRegistry, ToolWorkingSet}; + use client::CloudUserStore; use editor::EditorSettings; use fs::FakeFs; use gpui::{AppContext, TestAppContext, VisualTestContext}; @@ -4006,7 +4060,7 @@ mod tests { cx.run_until_parked(); - // Verify that the previous completion was canceled + // Verify that the previous completion was cancelled assert_eq!(cancellation_events.lock().unwrap().len(), 1); // Verify that a new request was started after cancellation @@ -4063,10 +4117,16 @@ mod tests { let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let (client, user_store) = + project.read_with(cx, |project, _cx| (project.client(), project.user_store())); + let cloud_user_store = + cx.new(|cx| CloudUserStore::new(client.cloud_client(), user_store, cx)); + let thread_store = cx .update(|_, cx| { ThreadStore::load( project.clone(), + cloud_user_store, cx.new(|_| ToolWorkingSet::default()), None, Arc::new(PromptBuilder::new(None).unwrap()), diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 224f49cc3e..fae04188eb 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -3,23 +3,18 @@ mod configure_context_server_modal; mod manage_profiles_modal; mod tool_picker; -use std::{ops::Range, sync::Arc, time::Duration}; +use std::{sync::Arc, time::Duration}; -use agent_servers::{AgentServerCommand, AgentServerSettings, AllAgentServersSettings, Gemini}; use agent_settings::AgentSettings; -use anyhow::Result; use assistant_tool::{ToolSource, ToolWorkingSet}; -use cloud_llm_client::Plan; 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, AsyncWindowContext, Corner, Entity, - EventEmitter, FocusHandle, Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation, - WeakEntity, percentage, + Action, Animation, AnimationExt as _, AnyView, App, Corner, Entity, EventEmitter, FocusHandle, + Focusable, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage, }; use language::LanguageRegistry; use language_model::{ @@ -27,24 +22,24 @@ use language_model::{ }; use notifications::status_toast::{StatusToast, ToastIcon}; use project::{ - Project, context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore}, project_settings::{ContextServerSettings, ProjectSettings}, }; -use settings::{Settings, SettingsStore, update_settings_file}; +use proto::Plan; +use settings::{Settings, update_settings_file}; use ui::{ Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu, Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*, }; use util::ResultExt as _; -use workspace::{Workspace, create_and_open_local_file}; +use workspace::Workspace; use zed_actions::ExtensionCategoryFilter; pub(crate) use configure_context_server_modal::ConfigureContextServerModal; pub(crate) use manage_profiles_modal::ManageProfilesModal; use crate::{ - AddContextServer, ExternalAgent, NewExternalAgentThread, + AddContextServer, agent_configuration::add_llm_provider_modal::{AddLlmProviderModal, LlmCompatibleProvider}, }; @@ -52,7 +47,6 @@ pub struct AgentConfiguration { fs: Arc, language_registry: Arc, workspace: WeakEntity, - project: WeakEntity, focus_handle: FocusHandle, configuration_views_by_provider: HashMap, context_server_store: Entity, @@ -62,8 +56,6 @@ pub struct AgentConfiguration { _registry_subscription: Subscription, scroll_handle: ScrollHandle, scrollbar_state: ScrollbarState, - gemini_is_installed: bool, - _check_for_gemini: Task<()>, } impl AgentConfiguration { @@ -73,7 +65,6 @@ impl AgentConfiguration { tools: Entity, language_registry: Arc, workspace: WeakEntity, - project: WeakEntity, window: &mut Window, cx: &mut Context, ) -> Self { @@ -98,34 +89,33 @@ impl AgentConfiguration { cx.subscribe(&context_server_store, |_, _, _, cx| cx.notify()) .detach(); - cx.observe_global_in::(window, |this, _, cx| { - this.check_for_gemini(cx); - cx.notify(); - }) - .detach(); 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, - project, focus_handle, configuration_views_by_provider: HashMap::default(), context_server_store, expanded_context_server_tools: HashMap::default(), - expanded_provider_configurations: HashMap::default(), + expanded_provider_configurations, tools, _registry_subscription: registry_subscription, scroll_handle, scrollbar_state, - gemini_is_installed: false, - _check_for_gemini: Task::ready(()), }; this.build_provider_configuration_views(window, cx); - this.check_for_gemini(cx); this } @@ -147,42 +137,10 @@ impl AgentConfiguration { window: &mut Window, cx: &mut Context, ) { - let configuration_view = provider.configuration_view( - language_model::ConfigurationViewTargetAgent::ZedAgent, - window, - cx, - ); + let configuration_view = provider.configuration_view(window, cx); self.configuration_views_by_provider .insert(provider.id(), configuration_view); } - - fn check_for_gemini(&mut self, cx: &mut Context) { - let project = self.project.clone(); - let settings = AllAgentServersSettings::get_global(cx).clone(); - self._check_for_gemini = cx.spawn({ - async move |this, cx| { - let Some(project) = project.upgrade() else { - return; - }; - let gemini_is_installed = AgentServerCommand::resolve( - Gemini::binary_name(), - &[], - // TODO expose fallback path from the Gemini/CC types so we don't have to hardcode it again here - None, - settings.gemini, - &project, - cx, - ) - .await - .is_some(); - this.update(cx, |this, cx| { - this.gemini_is_installed = gemini_is_installed; - cx.notify(); - }) - .ok(); - } - }); - } } impl Focusable for AgentConfiguration { @@ -203,8 +161,8 @@ impl AgentConfiguration { provider: &Arc, cx: &mut Context, ) -> impl IntoElement + use<> { - let provider_id = provider.id().0; - let provider_name = provider.name().0; + let provider_id = provider.id().0.clone(); + let provider_name = provider.name().0.clone(); let provider_id_string = SharedString::from(format!("provider-disclosure-{provider_id}")); let configuration_view = self @@ -222,7 +180,7 @@ impl AgentConfiguration { let current_plan = if is_zed_provider { self.workspace .upgrade() - .and_then(|workspace| workspace.read(cx).user_store().read(cx).plan()) + .and_then(|workspace| workspace.read(cx).user_store().read(cx).current_plan()) } else { None }; @@ -230,7 +188,7 @@ impl AgentConfiguration { let is_signed_in = self .workspace .read_with(cx, |workspace, _| { - !workspace.client().status().borrow().is_signed_out() + workspace.client().status().borrow().is_connected() }) .unwrap_or(false); @@ -257,6 +215,7 @@ impl AgentConfiguration { .child( h_flex() .id(provider_id_string.clone()) + .cursor_pointer() .px_2() .py_0p5() .w_full() @@ -276,7 +235,10 @@ impl AgentConfiguration { h_flex() .w_full() .gap_1() - .child(Label::new(provider_name.clone())) + .child( + Label::new(provider_name.clone()) + .size(LabelSize::Large), + ) .map(|this| { if is_zed_provider && is_signed_in { this.child( @@ -303,7 +265,7 @@ impl AgentConfiguration { .closed_icon(IconName::ChevronDown), ) .on_click(cx.listener({ - let provider_id = provider.id(); + let provider_id = provider.id().clone(); move |this, _event, _window, _cx| { let is_expanded = this .expanded_provider_configurations @@ -321,7 +283,7 @@ impl AgentConfiguration { "Start New Thread", ) .icon_position(IconPosition::Start) - .icon(IconName::Thread) + .icon(IconName::Plus) .icon_size(IconSize::Small) .icon_color(Color::Muted) .label_size(LabelSize::Small) @@ -338,7 +300,6 @@ impl AgentConfiguration { ) .child( div() - .w_full() .px_2() .when(is_expanded, |parent| match configuration_view { Some(configuration_view) => parent.child(configuration_view), @@ -420,7 +381,7 @@ impl AgentConfiguration { ), ) .child( - Label::new("Add at least one provider to use AI-powered features with Zed's native agent.") + Label::new("Add at least one provider to use AI-powered features.") .color(Color::Muted), ), ), @@ -445,9 +406,7 @@ impl AgentConfiguration { SwitchField::new( "always-allow-tool-actions-switch", "Allow running commands without asking for confirmation", - Some( - "The agent can perform potentially destructive actions without asking for your confirmation.".into(), - ), + "The agent can perform potentially destructive actions without asking for your confirmation.", always_allow_tool_actions, move |state, _window, cx| { let allow = state == &ToggleState::Selected; @@ -465,7 +424,7 @@ impl AgentConfiguration { SwitchField::new( "single-file-review", "Enable single-file agent reviews", - Some("Agent edits are also displayed in single-file editors for review.".into()), + "Agent edits are also displayed in single-file editors for review.", single_file_review, move |state, _window, cx| { let allow = state == &ToggleState::Selected; @@ -483,9 +442,7 @@ impl AgentConfiguration { SwitchField::new( "sound-notification", "Play sound when finished generating", - Some( - "Hear a notification sound when the agent is done generating changes or needs your input.".into(), - ), + "Hear a notification sound when the agent is done generating changes or needs your input.", play_sound_when_agent_done, move |state, _window, cx| { let allow = state == &ToggleState::Selected; @@ -503,9 +460,7 @@ impl AgentConfiguration { SwitchField::new( "modifier-send", "Use modifier to submit a message", - Some( - "Make a modifier (cmd-enter on macOS, ctrl-enter on Linux or Windows) required to send messages.".into(), - ), + "Make a modifier (cmd-enter on macOS, ctrl-enter on Linux) required to send messages.", use_modifier_to_send, move |state, _window, cx| { let allow = state == &ToggleState::Selected; @@ -547,7 +502,7 @@ 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::Free => ("Free", Color::Default, free_chip_bg), Plan::ZedProTrial => ("Pro Trial", Color::Accent, pro_chip_bg), Plan::ZedPro => ("Pro", Color::Accent, pro_chip_bg), }; @@ -561,14 +516,6 @@ impl AgentConfiguration { } } - fn card_item_bg_color(&self, cx: &mut Context) -> Hsla { - cx.theme().colors().background.opacity(0.25) - } - - fn card_item_border_color(&self, cx: &mut Context) -> Hsla { - cx.theme().colors().border.opacity(0.6) - } - fn render_context_servers_section( &mut self, window: &mut Window, @@ -586,12 +533,7 @@ impl AgentConfiguration { v_flex() .gap_0p5() .child(Headline::new("Model Context Protocol (MCP) Servers")) - .child( - Label::new( - "All context servers connected through the Model Context Protocol.", - ) - .color(Color::Muted), - ), + .child(Label::new("Connect to context servers via the Model Context Protocol either via Zed extensions or directly.").color(Color::Muted)), ) .children( context_server_ids.into_iter().map(|context_server_id| { @@ -601,7 +543,7 @@ impl AgentConfiguration { .child( h_flex() .justify_between() - .gap_1p5() + .gap_2() .child( h_flex().w_full().child( Button::new("add-context-server", "Add Custom Server") @@ -625,7 +567,7 @@ impl AgentConfiguration { .style(ButtonStyle::Filled) .layer(ElevationIndex::ModalSurface) .full_width() - .icon(IconName::ToolHammer) + .icon(IconName::Hammer) .icon_size(IconSize::Small) .icon_position(IconPosition::Start) .on_click(|_event, window, cx| { @@ -692,6 +634,8 @@ 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, @@ -710,7 +654,7 @@ impl AgentConfiguration { .size(IconSize::XSmall) .color(Color::Accent) .with_animation( - SharedString::from(format!("{}-starting", context_server_id.0,)), + SharedString::from(format!("{}-starting", context_server_id.0.clone(),)), Animation::new(Duration::from_secs(3)).repeat(), |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), ) @@ -834,8 +778,8 @@ impl AgentConfiguration { .id(item_id.clone()) .border_1() .rounded_md() - .border_color(self.card_item_border_color(cx)) - .bg(self.card_item_bg_color(cx)) + .border_color(border_color) + .bg(cx.theme().colors().background.opacity(0.2)) .overflow_hidden() .child( h_flex() @@ -843,11 +787,7 @@ impl AgentConfiguration { .justify_between() .when( error.is_some() || are_tools_expanded && tool_count >= 1, - |element| { - element - .border_b_1() - .border_color(self.card_item_border_color(cx)) - }, + |element| element.border_b_1().border_color(border_color), ) .child( h_flex() @@ -914,6 +854,7 @@ 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| { @@ -1006,7 +947,7 @@ impl AgentConfiguration { } parent.child(v_flex().py_1p5().px_1().gap_1().children( - tools.iter().enumerate().map(|(ix, tool)| { + tools.into_iter().enumerate().map(|(ix, tool)| { h_flex() .id(("tool-item", ix)) .px_1() @@ -1029,195 +970,6 @@ impl AgentConfiguration { )) }) } - - fn render_agent_servers_section(&mut self, cx: &mut Context) -> impl IntoElement { - let settings = AllAgentServersSettings::get_global(cx).clone(); - let user_defined_agents = settings - .custom - .iter() - .map(|(name, settings)| { - self.render_agent_server( - IconName::Ai, - name.clone(), - ExternalAgent::Custom { - name: name.clone(), - settings: settings.clone(), - }, - None, - cx, - ) - .into_any_element() - }) - .collect::>(); - - 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() - .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( - "Bring the agent of your choice to Zed via our new Agent Client Protocol.", - ) - .color(Color::Muted), - ), - ) - .child(self.render_agent_server( - IconName::AiGemini, - "Gemini CLI", - ExternalAgent::Gemini, - (!self.gemini_is_installed).then_some(Gemini::install_command().into()), - cx, - )) - // TODO add CC - .children(user_defined_agents), - ) - } - - fn render_agent_server( - &self, - icon: IconName, - name: impl Into, - agent: ExternalAgent, - install_command: Option, - cx: &mut Context, - ) -> 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())), - ) - .map(|this| { - if let Some(install_command) = install_command { - this.child( - Button::new( - SharedString::from(format!("install_external_agent-{name}")), - "Install Agent", - ) - .label_size(LabelSize::Small) - .icon(IconName::Plus) - .icon_position(IconPosition::Start) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .tooltip(Tooltip::text(install_command.clone())) - .on_click(cx.listener( - move |this, _, window, cx| { - let Some(project) = this.project.upgrade() else { - return; - }; - let Some(workspace) = this.workspace.upgrade() else { - return; - }; - let cwd = project.read(cx).first_project_directory(cx); - let shell = - project.read(cx).terminal_settings(&cwd, cx).shell.clone(); - let spawn_in_terminal = task::SpawnInTerminal { - id: task::TaskId(install_command.to_string()), - full_label: install_command.to_string(), - label: install_command.to_string(), - command: Some(install_command.to_string()), - args: Vec::new(), - command_label: install_command.to_string(), - cwd, - env: Default::default(), - use_new_terminal: true, - allow_concurrent_runs: true, - reveal: Default::default(), - reveal_target: Default::default(), - hide: Default::default(), - shell, - show_summary: true, - show_command: true, - show_rerun: false, - }; - let task = workspace.update(cx, |workspace, cx| { - workspace.spawn_in_terminal(spawn_in_terminal, window, cx) - }); - cx.spawn(async move |this, cx| { - task.await; - this.update(cx, |this, cx| { - this.check_for_gemini(cx); - }) - .ok(); - }) - .detach(); - }, - )), - ) - } else { - this.child( - h_flex().gap_1().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 { @@ -1237,7 +989,6 @@ 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)), ) @@ -1278,6 +1029,7 @@ 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() } @@ -1313,6 +1065,7 @@ 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) @@ -1356,109 +1109,3 @@ 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, - 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::() - .unwrap(); - - settings_editor - .downgrade() - .update_in(cx, |item, window, cx| { - let text = item.buffer().read(cx).snapshot(cx).text(); - - let settings = cx.global::(); - - let mut unique_server_name = None; - let edits = settings.edits_for_update::(&text, |file| { - let server_name: Option = (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, - AgentServerSettings { - command: AgentServerCommand { - path: "path_to_executable".into(), - args: vec![], - env: Some(HashMap::default()), - }, - }, - ); - } - }); - - if edits.is_empty() { - return; - } - - let ranges = edits - .iter() - .map(|(range, _)| range.clone()) - .collect::>(); - - 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> { - let chars = text.chars().collect::>(); - - 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 - } -} diff --git a/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs b/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs index 182831f488..94b32d156b 100644 --- a/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs +++ b/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs @@ -7,12 +7,10 @@ use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, T use language_model::LanguageModelRegistry; use language_models::{ AllLanguageModelSettings, OpenAiCompatibleSettingsContent, - provider::open_ai_compatible::{AvailableModel, ModelCapabilities}, + provider::open_ai_compatible::AvailableModel, }; use settings::update_settings_file; -use ui::{ - Banner, Checkbox, KeyBinding, Modal, ModalFooter, ModalHeader, Section, ToggleState, prelude::*, -}; +use ui::{Banner, KeyBinding, Modal, ModalFooter, ModalHeader, Section, prelude::*}; use ui_input::SingleLineInput; use workspace::{ModalView, Workspace}; @@ -71,19 +69,11 @@ 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, max_completion_tokens: Entity, max_output_tokens: Entity, max_tokens: Entity, - capabilities: ModelCapabilityToggles, } impl ModelInput { @@ -110,23 +100,11 @@ 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(), - }, } } @@ -158,12 +136,6 @@ impl ModelInput { .text(cx) .parse::() .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(), - }, }) } } @@ -300,34 +272,42 @@ impl AddLlmProviderModal { cx.emit(DismissEvent); } - fn render_model_section(&self, cx: &mut Context) -> impl IntoElement { - v_flex() - .mt_1() - .gap_2() - .child( - h_flex() - .justify_between() - .child(Label::new("Models").size(LabelSize::Small)) - .child( - Button::new("add-model", "Add Model") - .icon(IconName::Plus) - .icon_position(IconPosition::Start) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .label_size(LabelSize::Small) - .on_click(cx.listener(|this, _, window, cx| { - this.input.add_model(window, cx); - cx.notify(); - })), - ), - ) - .children( - self.input - .models - .iter() - .enumerate() - .map(|(ix, _)| self.render_model(ix, cx)), - ) + fn render_section(&self) -> Section { + Section::new() + .child(self.input.provider_name.clone()) + .child(self.input.api_url.clone()) + .child(self.input.api_key.clone()) + } + + fn render_model_section(&self, cx: &mut Context) -> Section { + Section::new().child( + v_flex() + .gap_2() + .child( + h_flex() + .justify_between() + .child(Label::new("Models").size(LabelSize::Small)) + .child( + Button::new("add-model", "Add Model") + .icon(IconName::Plus) + .icon_position(IconPosition::Start) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .label_size(LabelSize::Small) + .on_click(cx.listener(|this, _, window, cx| { + this.input.add_model(window, cx); + cx.notify(); + })), + ), + ) + .children( + self.input + .models + .iter() + .enumerate() + .map(|(ix, _)| self.render_model(ix, cx)), + ), + ) } fn render_model(&self, ix: usize, cx: &mut Context) -> impl IntoElement + use<> { @@ -350,55 +330,6 @@ 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") @@ -454,7 +385,7 @@ impl Render for AddLlmProviderModal { this.section( Section::new().child( Banner::new() - .severity(Severity::Warning) + .severity(ui::Severity::Warning) .child(div().text_xs().child(error)), ), ) @@ -462,14 +393,10 @@ impl Render for AddLlmProviderModal { .child( v_flex() .id("modal_content") - .size_full() .max_h_128() .overflow_y_scroll() - .px(DynamicSpacing::Base12.rems(cx)) - .gap(DynamicSpacing::Base04.rems(cx)) - .child(self.input.provider_name.clone()) - .child(self.input.api_url.clone()) - .child(self.input.api_key.clone()) + .gap_2() + .child(self.render_section()) .child(self.render_model_section(cx)), ) .footer( @@ -639,93 +566,6 @@ 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); diff --git a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs index c898a5acb5..06d035d836 100644 --- a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs +++ b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs @@ -163,10 +163,10 @@ impl ConfigurationSource { .read(cx) .text(cx); let settings = serde_json_lenient::from_str::(&text)?; - if let Some(settings_validator) = settings_validator - && let Err(error) = settings_validator.validate(&settings) - { - return Err(anyhow::anyhow!(error.to_string())); + if let Some(settings_validator) = settings_validator { + if let Err(error) = settings_validator.validate(&settings) { + return Err(anyhow::anyhow!(error.to_string())); + } } Ok(( id.clone(), @@ -261,6 +261,7 @@ impl ConfigureContextServerModal { _cx: &mut Context, ) { 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(); @@ -437,7 +438,7 @@ impl ConfigureContextServerModal { format!("{} configured successfully.", id.0), cx, |this, _cx| { - this.icon(ToastIcon::new(IconName::ToolHammer).color(Color::Muted)) + this.icon(ToastIcon::new(IconName::Hammer).color(Color::Muted)) .action("Dismiss", |_, _| {}) }, ); @@ -486,7 +487,7 @@ impl ConfigureContextServerModal { } fn render_modal_description(&self, window: &mut Window, cx: &mut Context) -> AnyElement { - const MODAL_DESCRIPTION: &str = "Visit the MCP server configuration docs to find all necessary arguments and environment variables."; + const MODAL_DESCRIPTION: &'static str = "Visit the MCP server configuration docs to find all necessary arguments and environment variables."; if let ConfigurationSource::Extension { installation_instructions: Some(installation_instructions), @@ -566,7 +567,7 @@ impl ConfigureContextServerModal { Button::new("open-repository", "Open Repository") .icon(IconName::ArrowUpRight) .icon_color(Color::Muted) - .icon_size(IconSize::Small) + .icon_size(IconSize::XSmall) .tooltip({ let repository_url = repository_url.clone(); move |window, cx| { @@ -715,24 +716,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 - && let Some(tx) = tx.lock().unwrap().take() - { - let _ = tx.send(Ok(())); + if server_id == &context_server_id { + if let Some(tx) = tx.lock().unwrap().take() { + let _ = tx.send(Ok(())); + } } } ContextServerStatus::Stopped => { - if server_id == &context_server_id - && let Some(tx) = tx.lock().unwrap().take() - { - let _ = tx.send(Err("Context server stopped running".into())); + if server_id == &context_server_id { + if 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 - && let Some(tx) = tx.lock().unwrap().take() - { - let _ = tx.send(Err(error.clone())); + if server_id == &context_server_id { + if let Some(tx) = tx.lock().unwrap().take() { + let _ = tx.send(Err(error.clone())); + } } } _ => {} diff --git a/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs b/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs index 7fcf76d1cb..45536ff13b 100644 --- a/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs +++ b/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs @@ -464,7 +464,7 @@ impl ManageProfilesModal { }, )) .child(ListSeparator) - .child(h_flex().p_2().child(mode.name_editor)) + .child(h_flex().p_2().child(mode.name_editor.clone())) } fn render_view_profile( @@ -483,7 +483,7 @@ impl ManageProfilesModal { let icon = match mode.profile_id.as_str() { "write" => IconName::Pencil, - "ask" => IconName::Chat, + "ask" => IconName::MessageBubbles, _ => IconName::UserRoundPen, }; @@ -594,7 +594,7 @@ impl ManageProfilesModal { .inset(true) .spacing(ListItemSpacing::Sparse) .start_slot( - Icon::new(IconName::ToolHammer) + Icon::new(IconName::Hammer) .size(IconSize::Small) .color(Color::Muted), ) @@ -763,7 +763,7 @@ impl Render for ManageProfilesModal { .pb_1() .child(ProfileModalHeader::new( format!("{profile_name} — Configure MCP Tools"), - Some(IconName::ToolHammer), + Some(IconName::Hammer), )) .child(ListSeparator) .child(tool_picker.clone()) diff --git a/crates/agent_ui/src/agent_configuration/tool_picker.rs b/crates/agent_ui/src/agent_configuration/tool_picker.rs index 25947a1e58..8f1e0d71c0 100644 --- a/crates/agent_ui/src/agent_configuration/tool_picker.rs +++ b/crates/agent_ui/src/agent_configuration/tool_picker.rs @@ -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() - && name.contains(&query) - { - tools_by_provider.entry(server_id).or_default().push(name); + if let PickerItem::Tool { server_id, name } = item.clone() { + if name.contains(&query) { + tools_by_provider.entry(server_id).or_default().push(name); + } } } diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index 1e1ff95178..5c8011cb18 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -1,9 +1,9 @@ use crate::{Keep, KeepAll, OpenAgentDiff, Reject, RejectAll}; use acp_thread::{AcpThread, AcpThreadEvent}; -use action_log::ActionLog; use agent::{Thread, ThreadEvent, ThreadSummary}; use agent_settings::AgentSettings; use anyhow::Result; +use assistant_tool::ActionLog; use buffer_diff::DiffHunkStatus; use collections::{HashMap, HashSet}; use editor::{ @@ -185,7 +185,7 @@ impl AgentDiffPane { let focus_handle = cx.focus_handle(); let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); - let project = thread.project(cx); + let project = thread.project(cx).clone(); let editor = cx.new(|cx| { let mut editor = Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx); @@ -196,24 +196,27 @@ impl AgentDiffPane { editor }); - let action_log = thread.action_log(cx); + let action_log = thread.action_log(cx).clone(); let mut this = Self { - _subscriptions: vec![ - cx.observe_in(&action_log, window, |this, _action_log, window, cx| { - this.update_excerpts(window, cx) - }), + _subscriptions: [ + Some( + cx.observe_in(&action_log, window, |this, _action_log, window, cx| { + this.update_excerpts(window, cx) + }), + ), match &thread { - 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) - }), + AgentDiffThread::Native(thread) => { + Some(cx.subscribe(&thread, |this, _thread, event, cx| { + this.handle_thread_event(event, cx) + })) + } + AgentDiffThread::AcpThread(_) => None, }, - ], + ] + .into_iter() + .flatten() + .collect(), title: SharedString::default(), multibuffer, editor, @@ -285,7 +288,7 @@ impl AgentDiffPane { && buffer .read(cx) .file() - .is_some_and(|file| file.disk_state() == DiskState::Deleted) + .map_or(false, |file| file.disk_state() == DiskState::Deleted) { editor.fold_buffer(snapshot.text.remote_id(), cx) } @@ -321,15 +324,10 @@ impl AgentDiffPane { } } - fn handle_native_thread_event(&mut self, event: &ThreadEvent, cx: &mut Context) { - if let ThreadEvent::SummaryGenerated = event { - self.update_title(cx) - } - } - - fn handle_acp_thread_event(&mut self, event: &AcpThreadEvent, cx: &mut Context) { - if let AcpThreadEvent::TitleUpdated = event { - self.update_title(cx) + fn handle_thread_event(&mut self, event: &ThreadEvent, cx: &mut Context) { + match event { + ThreadEvent::SummaryGenerated => self.update_title(cx), + _ => {} } } @@ -400,7 +398,7 @@ fn keep_edits_in_selection( .disjoint_anchor_ranges() .collect::>(); - 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( @@ -414,7 +412,7 @@ fn reject_edits_in_selection( .selections .disjoint_anchor_ranges() .collect::>(); - 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( @@ -505,7 +503,8 @@ fn update_editor_selection( &[last_kept_hunk_end..editor::Anchor::max()], buffer_snapshot, ) - .nth(1) + .skip(1) + .next() }) .or_else(|| { let first_kept_hunk = diff_hunks.first()?; @@ -1002,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(); } @@ -1045,23 +1044,23 @@ impl ToolbarItemView for AgentDiffToolbar { return self.location(cx); } - if let Some(editor) = item.act_as::(cx) - && editor.read(cx).mode().is_full() - { - let agent_diff = AgentDiff::global(cx); + if let Some(editor) = item.act_as::(cx) { + if 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; - self.location(cx) + return self.location(cx); } fn pane_focus_update( @@ -1312,7 +1311,7 @@ impl AgentDiff { let entity = cx.new(|_cx| Self::default()); let global = AgentDiffGlobal(entity.clone()); cx.set_global(global); - entity + entity.clone() }) } @@ -1334,7 +1333,7 @@ impl AgentDiff { window: &mut Window, cx: &mut Context, ) { - let action_log = thread.action_log(cx); + let action_log = thread.action_log(cx).clone(); let action_log_subscription = cx.observe_in(&action_log, window, { let workspace = workspace.clone(); @@ -1344,13 +1343,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) @@ -1358,11 +1357,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; } @@ -1507,7 +1506,7 @@ impl AgentDiff { .read(cx) .entries() .last() - .is_some_and(|entry| entry.diffs().next().is_some()) + .map_or(false, |entry| entry.diffs().next().is_some()) { self.update_reviewing_editors(workspace, window, cx); } @@ -1517,20 +1516,11 @@ impl AgentDiff { .read(cx) .entries() .get(*ix) - .is_some_and(|entry| entry.diffs().next().is_some()) + .map_or(false, |entry| entry.diffs().next().is_some()) { self.update_reviewing_editors(workspace, window, cx); } } - AcpThreadEvent::Stopped | AcpThreadEvent::Error | AcpThreadEvent::LoadError(_) => { - self.update_reviewing_editors(workspace, window, cx); - } - AcpThreadEvent::TitleUpdated - | AcpThreadEvent::TokenUsageUpdated - | AcpThreadEvent::EntriesRemoved(_) - | AcpThreadEvent::ToolAuthorizationRequired - | AcpThreadEvent::PromptCapabilitiesUpdated - | AcpThreadEvent::Retry(_) => {} } } @@ -1541,11 +1531,21 @@ impl AgentDiff { window: &mut Window, cx: &mut Context, ) { - if let workspace::Event::ItemAdded { item } = event - && let Some(editor) = item.downcast::() - && let Some(buffer) = Self::full_editor_buffer(editor.read(cx), cx) - { - self.register_editor(workspace.downgrade(), buffer, editor, window, cx); + match event { + workspace::Event::ItemAdded { item } => { + if let Some(editor) = item.downcast::() { + if let Some(buffer) = Self::full_editor_buffer(editor.read(cx), cx) { + self.register_editor( + workspace.downgrade(), + buffer.clone(), + editor, + window, + cx, + ); + } + } + } + _ => {} } } @@ -1644,7 +1644,7 @@ impl AgentDiff { continue; }; - for weak_editor in buffer_editors.keys() { + for (weak_editor, _) in buffer_editors { let Some(editor) = weak_editor.upgrade() else { continue; }; @@ -1672,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) { @@ -1705,7 +1705,7 @@ impl AgentDiff { .read_with(cx, |editor, _cx| editor.workspace()) .ok() .flatten() - .is_some_and(|editor_workspace| { + .map_or(false, |editor_workspace| { editor_workspace.entity_id() == workspace.entity_id() }); @@ -1725,7 +1725,7 @@ impl AgentDiff { fn editor_state(&self, editor: &WeakEntity) -> EditorState { self.reviewing_editors - .get(editor) + .get(&editor) .cloned() .unwrap_or(EditorState::Idle) } @@ -1845,26 +1845,26 @@ impl AgentDiff { let thread = thread.upgrade()?; - 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); + 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)); + 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 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); + } } } - Some(Task::ready(Ok(()))) + return Some(Task::ready(Ok(()))); } } @@ -1893,6 +1893,7 @@ mod tests { use agent::thread_store::{self, ThreadStore}; use agent_settings::AgentSettings; use assistant_tool::ToolWorkingSet; + use client::CloudUserStore; use editor::EditorSettings; use gpui::{TestAppContext, UpdateGlobal, VisualTestContext}; use project::{FakeFs, Project}; @@ -1932,11 +1933,17 @@ mod tests { }) .unwrap(); + let (client, user_store) = + project.read_with(cx, |project, _cx| (project.client(), project.user_store())); + let cloud_user_store = + cx.new(|cx| CloudUserStore::new(client.cloud_client(), user_store, cx)); + let prompt_store = None; let thread_store = cx .update(|cx| { ThreadStore::load( project.clone(), + cloud_user_store, cx.new(|_| ToolWorkingSet::default()), prompt_store, Arc::new(PromptBuilder::new(None).unwrap()), @@ -2098,11 +2105,17 @@ mod tests { }) .unwrap(); + let (client, user_store) = + project.read_with(cx, |project, _cx| (project.client(), project.user_store())); + let cloud_user_store = + cx.new(|cx| CloudUserStore::new(client.cloud_client(), user_store, cx)); + let prompt_store = None; let thread_store = cx .update(|cx| { ThreadStore::load( project.clone(), + cloud_user_store, cx.new(|_| ToolWorkingSet::default()), prompt_store, Arc::new(PromptBuilder::new(None).unwrap()), diff --git a/crates/agent_ui/src/agent_model_selector.rs b/crates/agent_ui/src/agent_model_selector.rs index 3de1027d91..b989e7bf1e 100644 --- a/crates/agent_ui/src/agent_model_selector.rs +++ b/crates/agent_ui/src/agent_model_selector.rs @@ -66,8 +66,10 @@ impl AgentModelSelector { fs.clone(), cx, move |settings, _cx| { - settings - .set_inline_assistant_model(provider.clone(), model_id); + settings.set_inline_assistant_model( + provider.clone(), + model_id.clone(), + ); }, ); } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index d1cf748733..7e0d766f91 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -1,26 +1,23 @@ +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 acp_thread::AcpThread; -use agent_servers::AgentServerSettings; -use agent2::{DbThreadMetadata, HistoryEntry}; +use agent_servers::AgentServer; use db::kvp::{Dismissable, KEY_VALUE_STORE}; use serde::{Deserialize, Serialize}; -use zed_actions::OpenBrowser; -use zed_actions::agent::ReauthenticateAgent; -use crate::acp::{AcpThreadHistory, ThreadHistoryEvent}; +use crate::NewExternalAgentThread; use crate::agent_diff::AgentDiffThread; -use crate::ui::AcpOnboardingModal; +use crate::message_editor::{MAX_EDITOR_LINES, MIN_EDITOR_LINES}; +use crate::ui::NewThreadButton; use crate::{ AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode, DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ResetTrialEndUpsell, - ResetTrialUpsell, ToggleBurnMode, ToggleContextPicker, ToggleNavigationMenu, - ToggleNewThreadMenu, ToggleOptionsMenu, + ResetTrialUpsell, ToggleBurnMode, ToggleContextPicker, ToggleNavigationMenu, ToggleOptionsMenu, acp::AcpThreadView, active_thread::{self, ActiveThread, ActiveThreadEvent}, agent_configuration::{AgentConfiguration, AssistantConfigurationEvent}, @@ -34,7 +31,6 @@ use crate::{ thread_history::{HistoryEntryElement, ThreadHistory}, ui::{AgentOnboardingModal, EndTrialUpsell}, }; -use crate::{ExternalAgent, NewExternalAgentThread, NewNativeAgentThreadFromSummary}; use agent::{ Thread, ThreadError, ThreadEvent, ThreadId, ThreadSummary, TokenUsageRatio, context_store::ContextStore, @@ -47,20 +43,24 @@ use anyhow::{Result, anyhow}; 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 client::{CloudUserStore, DisableAiSettings, UserStore, zed_urls}; +use cloud_llm_client::{CompletionIntent, UsageLimit}; use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer}; -use feature_flags::{self, ClaudeCodeFeatureFlag, FeatureFlagAppExt, GeminiAndNativeFeatureFlag}; +use feature_flags::{self, FeatureFlagAppExt}; use fs::Fs; use gpui::{ Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem, - Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, KeyContext, - Pixels, Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between, + Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, Hsla, + KeyContext, Pixels, Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, + pulsating_between, }; use language::LanguageRegistry; -use language_model::{ConfigurationError, ConfiguredModel, LanguageModelRegistry}; -use project::{DisableAiSettings, Project, ProjectPath, Worktree}; +use language_model::{ + ConfigurationError, ConfiguredModel, LanguageModelProviderTosView, LanguageModelRegistry, +}; +use project::{Project, ProjectPath, Worktree}; use prompt_store::{PromptBuilder, PromptStore, UserPromptId}; +use proto::Plan; use rules_library::{RulesLibrary, open_rules_library}; use search::{BufferSearchBar, buffer_search}; use settings::{Settings, update_settings_file}; @@ -78,10 +78,7 @@ use workspace::{ }; use zed_actions::{ DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize, - agent::{ - OpenAcpOnboardingModal, OpenOnboardingModal, OpenSettings, ResetOnboarding, - ToggleModelSelector, - }, + agent::{OpenOnboardingModal, OpenSettings, ResetOnboarding, ToggleModelSelector}, assistant::{OpenRulesLibrary, ToggleFocus}, }; @@ -90,7 +87,6 @@ const AGENT_PANEL_KEY: &str = "agent_panel"; #[derive(Serialize, Deserialize)] struct SerializedAgentPanel { width: Option, - selected_agent: Option, } pub fn init(cx: &mut App) { @@ -103,16 +99,6 @@ pub fn init(cx: &mut App) { workspace.focus_panel::(window, cx); } }) - .register_action( - |workspace, action: &NewNativeAgentThreadFromSummary, window, cx| { - if let Some(panel) = workspace.panel::(cx) { - panel.update(cx, |panel, cx| { - panel.new_native_agent_thread_from_summary(action, window, cx) - }); - workspace.focus_panel::(window, cx); - } - }, - ) .register_action(|workspace, _: &OpenHistory, window, cx| { if let Some(panel) = workspace.panel::(cx) { workspace.focus_panel::(window, cx); @@ -135,7 +121,7 @@ pub fn init(cx: &mut App) { if let Some(panel) = workspace.panel::(cx) { workspace.focus_panel::(window, cx); panel.update(cx, |panel, cx| { - panel.external_thread(action.agent.clone(), None, None, window, cx) + panel.new_external_thread(action.agent, window, cx) }); } }) @@ -194,20 +180,9 @@ pub fn init(cx: &mut App) { }); } }) - .register_action(|workspace, _: &ToggleNewThreadMenu, window, cx| { - if let Some(panel) = workspace.panel::(cx) { - workspace.focus_panel::(window, cx); - panel.update(cx, |panel, cx| { - panel.toggle_new_thread_menu(&ToggleNewThreadMenu, window, cx); - }); - } - }) .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, _: &ResetOnboarding, window, cx| { window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx); window.refresh(); @@ -249,42 +224,6 @@ enum WhichFontSize { None, } -// TODO unify this with ExternalAgent -#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] -pub enum AgentType { - #[default] - Zed, - TextThread, - Gemini, - ClaudeCode, - NativeAgent, - Custom { - name: SharedString, - settings: AgentServerSettings, - }, -} - -impl AgentType { - fn label(&self) -> SharedString { - match self { - 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) -> Option { - match self { - Self::Zed | Self::NativeAgent | Self::TextThread => None, - Self::Gemini => Some(IconName::AiGemini), - Self::ClaudeCode => Some(IconName::AiClaude), - Self::Custom { .. } => Some(IconName::Terminal), - } - } -} - impl ActiveView { pub fn which_font_size_used(&self) -> WhichFontSize { match self { @@ -379,7 +318,7 @@ impl ActiveView { Self::Thread { change_title_editor: editor, thread: active_thread, - message_editor, + message_editor: message_editor, _subscriptions: subscriptions, } } @@ -387,7 +326,6 @@ impl ActiveView { pub fn prompt_editor( context_editor: Entity, history_store: Entity, - acp_history_store: Entity, language_registry: Arc, window: &mut Window, cx: &mut App, @@ -465,18 +403,6 @@ 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, - ); - } - }); } _ => {} } @@ -501,12 +427,11 @@ impl ActiveView { pub struct AgentPanel { workspace: WeakEntity, user_store: Entity, + cloud_user_store: Entity, project: Entity, fs: Arc, language_registry: Arc, thread_store: Entity, - acp_history: Entity, - acp_history_store: Entity, _default_model_subscription: Subscription, context_store: Entity, prompt_store: Option>, @@ -515,6 +440,8 @@ pub struct AgentPanel { configuration_subscription: Option, local_timezone: UtcOffset, active_view: ActiveView, + acp_message_history: + Rc>>>, previous_view: Option, history_store: Entity, history: Entity, @@ -528,27 +455,21 @@ pub struct AgentPanel { zoomed: bool, pending_serialization: Option>>, onboarding: Entity, - selected_agent: AgentType, } impl AgentPanel { fn serialize(&mut self, cx: &mut Context) { let width = self.width; - let selected_agent = self.selected_agent.clone(); self.pending_serialization = Some(cx.background_spawn(async move { KEY_VALUE_STORE .write_kvp( AGENT_PANEL_KEY.into(), - serde_json::to_string(&SerializedAgentPanel { - width, - selected_agent: Some(selected_agent), - })?, + serde_json::to_string(&SerializedAgentPanel { width })?, ) .await?; anyhow::Ok(()) })); } - pub fn load( workspace: WeakEntity, prompt_builder: Arc, @@ -566,6 +487,7 @@ impl AgentPanel { let project = workspace.project().clone(); ThreadStore::load( project, + workspace.app_state().cloud_user_store.clone(), tools.clone(), prompt_store.clone(), prompt_builder.clone(), @@ -612,10 +534,6 @@ impl AgentPanel { if let Some(serialized_panel) = serialized_panel { 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.clone(); - panel.new_agent_thread(selected_agent, window, cx); - } cx.notify(); }); } @@ -637,6 +555,7 @@ impl AgentPanel { let thread = thread_store.update(cx, |this, cx| this.create_thread(cx)); let fs = workspace.app_state().fs.clone(); let user_store = workspace.app_state().user_store.clone(); + let cloud_user_store = workspace.app_state().cloud_user_store.clone(); let project = workspace.project(); let language_registry = project.read(cx).languages().clone(); let client = workspace.client().clone(); @@ -663,6 +582,7 @@ impl AgentPanel { MessageEditor::new( fs.clone(), workspace.clone(), + cloud_user_store.clone(), message_editor_context_store.clone(), prompt_store.clone(), thread_store.downgrade(), @@ -674,29 +594,6 @@ 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| { @@ -735,7 +632,6 @@ impl AgentPanel { ActiveView::prompt_editor( context_editor, history_store.clone(), - acp_history_store.clone(), language_registry.clone(), window, cx, @@ -752,11 +648,7 @@ impl AgentPanel { let assistant_navigation_menu = ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| { if let Some(panel) = panel.upgrade() { - if cx.has_flag::() { - menu = Self::populate_recently_opened_menu_section_new(menu, panel, cx); - } else { - menu = Self::populate_recently_opened_menu_section_old(menu, panel, cx); - } + menu = Self::populate_recently_opened_menu_section(menu, panel, cx); } menu.action("View All", Box::new(OpenHistory)) .end_slot_action(DeleteRecentlyOpenThread.boxed_clone()) @@ -782,29 +674,30 @@ impl AgentPanel { .ok(); }); - 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 => {} - } + 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)); } + ActiveView::ExternalAgentThread { .. } + | ActiveView::TextThread { .. } + | ActiveView::History + | ActiveView::Configuration => {} }, - ); + _ => {} + }, + ); let onboarding = cx.new(|cx| { AgentPanelOnboarding::new( user_store.clone(), + cloud_user_store.clone(), client, |_window, cx| { OnboardingUpsell::set_dismissed(true, cx); @@ -817,6 +710,7 @@ impl AgentPanel { active_view, workspace, user_store, + cloud_user_store, project: project.clone(), fs: fs.clone(), language_registry, @@ -832,6 +726,7 @@ 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, @@ -844,9 +739,6 @@ impl AgentPanel { zoomed: false, pending_serialization: None, onboarding, - acp_history, - acp_history_store, - selected_agent: AgentType::default(), } } @@ -890,10 +782,10 @@ impl AgentPanel { ActiveView::Thread { thread, .. } => { thread.update(cx, |thread, cx| thread.cancel_last_completion(window, cx)); } - ActiveView::ExternalAgentThread { .. } - | ActiveView::TextThread { .. } - | ActiveView::History - | ActiveView::Configuration => {} + ActiveView::ExternalAgentThread { thread_view, .. } => { + thread_view.update(cx, |thread_element, cx| thread_element.cancel(cx)); + } + ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {} } } @@ -907,20 +799,7 @@ impl AgentPanel { } } - fn active_thread_view(&self) -> Option<&Entity> { - 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) { - if cx.has_flag::() { - 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() @@ -974,6 +853,7 @@ impl AgentPanel { MessageEditor::new( self.fs.clone(), self.workspace.clone(), + self.cloud_user_store.clone(), context_store.clone(), self.prompt_store.clone(), self.thread_store.downgrade(), @@ -993,38 +873,13 @@ impl AgentPanel { message_editor.focus_handle(cx).focus(window); - let thread_view = ActiveView::thread(active_thread, message_editor, window, cx); + let thread_view = ActiveView::thread(active_thread.clone(), 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, - ) { - 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) { - telemetry::event!("Agent Thread Started", agent = "zed-text"); - let context = self .context_store .update(cx, |context_store, cx| context_store.create(cx)); @@ -1050,7 +905,6 @@ impl AgentPanel { ActiveView::prompt_editor( context_editor.clone(), self.history_store.clone(), - self.acp_history_store.clone(), self.language_registry.clone(), window, cx, @@ -1061,17 +915,15 @@ impl AgentPanel { context_editor.focus_handle(cx).focus(window); } - fn external_thread( + fn new_external_thread( &mut self, agent_choice: Option, - resume_thread: Option, - summarize_thread: Option, window: &mut Window, cx: &mut Context, ) { let workspace = self.workspace.clone(); let project = self.project.clone(); - let fs = self.fs.clone(); + let message_history = self.acp_message_history.clone(); const LAST_USED_EXTERNAL_AGENT_KEY: &str = "agent_panel__last_used_external_agent"; @@ -1080,30 +932,25 @@ impl AgentPanel { agent: crate::ExternalAgent, } - let history = self.acp_history_store.clone(); - cx.spawn_in(window, async move |this, cx| { - let ext_agent = match agent_choice { + let server: Rc = match agent_choice { Some(agent) => { - 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(); - } + 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(); } }) .detach(); - agent + agent.server() } - None => { - cx.background_spawn(async move { + None => cx + .background_spawn(async move { KEY_VALUE_STORE.read_kvp(LAST_USED_EXTERNAL_AGENT_KEY) }) .await @@ -1114,44 +961,30 @@ impl AgentPanel { }) .unwrap_or_default() .agent - } + .server(), }; - 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::() { - return; - } - } - crate::ExternalAgent::ClaudeCode => { - if !cx.has_flag::() { - return; - } - } - } - let thread_view = cx.new(|cx| { crate::acp::AcpThreadView::new( server, - resume_thread, - summarize_thread, workspace.clone(), project, - this.acp_history_store.clone(), - this.prompt_store.clone(), + message_history, + MIN_EDITOR_LINES, + Some(MAX_EDITOR_LINES), window, cx, ) }); - this.set_active_view(ActiveView::ExternalAgentThread { thread_view }, window, cx); + this.set_active_view( + ActiveView::ExternalAgentThread { + thread_view: thread_view.clone(), + }, + window, + cx, + ); }) }) .detach_and_log_err(cx); @@ -1234,9 +1067,8 @@ impl AgentPanel { }); self.set_active_view( ActiveView::prompt_editor( - editor, + editor.clone(), self.history_store.clone(), - self.acp_history_store.clone(), self.language_registry.clone(), window, cx, @@ -1295,6 +1127,7 @@ impl AgentPanel { MessageEditor::new( self.fs.clone(), self.workspace.clone(), + self.cloud_user_store.clone(), context_store, self.prompt_store.clone(), self.thread_store.downgrade(), @@ -1307,7 +1140,7 @@ impl AgentPanel { }); message_editor.focus_handle(cx).focus(window); - let thread_view = ActiveView::thread(active_thread, message_editor, window, cx); + let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx); self.set_active_view(thread_view, window, cx); AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx); } @@ -1355,15 +1188,6 @@ impl AgentPanel { self.agent_panel_menu_handle.toggle(window, cx); } - pub fn toggle_new_thread_menu( - &mut self, - _: &ToggleNewThreadMenu, - window: &mut Window, - cx: &mut Context, - ) { - self.new_thread_menu_handle.toggle(window, cx); - } - pub fn increase_font_size( &mut self, action: &IncreaseBufferFontSize, @@ -1394,11 +1218,13 @@ impl AgentPanel { ThemeSettings::get_global(cx).agent_font_size(cx) + delta; let _ = settings .agent_font_size - .insert(Some(theme::clamp_font_size(agent_font_size).into())); + .insert(theme::clamp_font_size(agent_font_size).0); }, ); } else { - theme::adjust_agent_font_size(cx, |size| size + delta); + theme::adjust_agent_font_size(cx, |size| { + *size += delta; + }); } } WhichFontSize::BufferFont => { @@ -1476,7 +1302,6 @@ impl AgentPanel { tools, self.language_registry.clone(), self.workspace.clone(), - self.project.downgrade(), window, cx, ) @@ -1535,14 +1360,15 @@ impl AgentPanel { AssistantConfigurationEvent::NewThread(provider) => { if LanguageModelRegistry::read_global(cx) .default_model() - .is_none_or(|model| model.provider.id() != provider.id()) - && let Some(model) = provider.default_model(cx) + .map_or(true, |model| model.provider.id() != provider.id()) { - update_settings_file::( - self.fs.clone(), - cx, - move |settings, _| settings.set_model(model), - ); + if let Some(model) = provider.default_model(cx) { + update_settings_file::( + self.fs.clone(), + cx, + move |settings, _| settings.set_model(model), + ); + } } self.new_thread(&NewThread::default(), window, cx); @@ -1569,14 +1395,6 @@ impl AgentPanel { _ => None, } } - pub(crate) fn active_agent_thread(&self, cx: &App) -> Option> { - match &self.active_view { - ActiveView::ExternalAgentThread { thread_view, .. } => { - thread_view.read(cx).thread().cloned() - } - _ => None, - } - } pub(crate) fn delete_thread( &mut self, @@ -1597,7 +1415,7 @@ impl AgentPanel { return; } - let model = thread_state.configured_model().map(|cm| cm.model); + let model = thread_state.configured_model().map(|cm| cm.model.clone()); if let Some(model) = model { thread.update(cx, |active_thread, cx| { active_thread.thread().update(cx, |thread, cx| { @@ -1669,14 +1487,17 @@ impl AgentPanel { let current_is_special = current_is_history || current_is_config; let new_is_special = new_is_history || new_is_config; - 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 &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); + }); + } } + _ => {} } match &new_view { @@ -1689,14 +1510,6 @@ 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 { .. } => {} @@ -1714,10 +1527,12 @@ 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_old( + fn populate_recently_opened_menu_section( mut menu: ContextMenu, panel: Entity, cx: &mut Context, @@ -1752,7 +1567,7 @@ impl AgentPanel { .open_thread_by_id(&id, window, cx) .detach_and_log_err(cx), HistoryEntryId::Context(path) => this - .open_saved_prompt_editor(path, window, cx) + .open_saved_prompt_editor(path.clone(), window, cx) .detach_and_log_err(cx), }) .ok(); @@ -1780,143 +1595,6 @@ impl AgentPanel { menu } - - fn populate_recently_opened_menu_section_new( - mut menu: ContextMenu, - panel: Entity, - cx: &mut Context, - ) -> 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.clone() - } - - pub fn new_agent_thread( - &mut self, - agent: AgentType, - window: &mut Window, - cx: &mut Context, - ) { - if self.selected_agent != agent { - self.selected_agent = agent.clone(); - self.serialize(cx); - } - - 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.external_thread( - Some(crate::ExternalAgent::ClaudeCode), - None, - None, - window, - cx, - ), - AgentType::Custom { name, settings } => self.external_thread( - Some(crate::ExternalAgent::Custom { name, settings }), - None, - None, - window, - cx, - ), - } - } - - pub fn load_agent_thread( - &mut self, - thread: DbThreadMetadata, - window: &mut Window, - cx: &mut Context, - ) { - self.external_thread( - Some(ExternalAgent::NativeAgent), - Some(thread), - None, - window, - cx, - ); - } } impl Focusable for AgentPanel { @@ -1924,13 +1602,7 @@ 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 => { - if cx.has_flag::() { - self.acp_history.focus_handle(cx) - } else { - self.history.focus_handle(cx) - } - } + ActiveView::History => self.history.focus_handle(cx), ActiveView::TextThread { context_editor, .. } => context_editor.focus_handle(cx), ActiveView::Configuration => { if let Some(configuration) = self.configuration.as_ref() { @@ -2052,13 +1724,11 @@ impl AgentPanel { }; match state { - ThreadSummary::Pending => Label::new(ThreadSummary::DEFAULT) + ThreadSummary::Pending => Label::new(ThreadSummary::DEFAULT.clone()) .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() @@ -2068,8 +1738,7 @@ impl AgentPanel { .w_full() .child(change_title_editor.clone()) .child( - IconButton::new("retry-summary-generation", IconName::RotateCcw) - .icon_size(IconSize::Small) + ui::IconButton::new("retry-summary-generation", IconName::RotateCcw) .on_click({ let active_thread = active_thread.clone(); move |_, _window, cx| { @@ -2090,33 +1759,9 @@ impl AgentPanel { } } ActiveView::ExternalAgentThread { thread_view } => { - 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()) - .color(Color::Muted) - .truncate() - .into_any_element() - } + Label::new(thread_view.read(cx).title(cx)) + .truncate() + .into_any_element() } ActiveView::TextThread { title_editor, @@ -2127,7 +1772,6 @@ impl AgentPanel { match summary { ContextSummary::Pending => Label::new(ContextSummary::DEFAULT) - .color(Color::Muted) .truncate() .into_any_element(), ContextSummary::Content(summary) => { @@ -2139,7 +1783,6 @@ impl AgentPanel { } else { Label::new(LOADING_SUMMARY_PLACEHOLDER) .truncate() - .color(Color::Muted) .into_any_element() } } @@ -2147,8 +1790,7 @@ impl AgentPanel { .w_full() .child(title_editor.clone()) .child( - IconButton::new("retry-summary-generation", IconName::RotateCcw) - .icon_size(IconSize::Small) + ui::IconButton::new("retry-summary-generation", IconName::RotateCcw) .on_click({ let context_editor = context_editor.clone(); move |_, _window, cx| { @@ -2183,26 +1825,198 @@ impl AgentPanel { .into_any() } - fn render_panel_options_menu( - &self, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let user_store = self.user_store.read(cx); - let usage = user_store.model_request_usage(); + fn render_toolbar(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let cloud_user_store = self.cloud_user_store.read(cx); + let usage = cloud_user_store.model_request_usage(); + let account_url = zed_urls::account_url(cx); let focus_handle = self.focus_handle(cx); - let full_screen_label = if self.is_zoomed(window, cx) { - "Disable Full Screen" + let go_back_button = div().child( + IconButton::new("go-back", IconName::ArrowLeft) + .icon_size(IconSize::Small) + .on_click(cx.listener(|this, _, window, cx| { + this.go_back(&workspace::GoBack, window, cx); + })) + .tooltip({ + let focus_handle = focus_handle.clone(); + move |window, cx| { + Tooltip::for_action_in( + "Go Back", + &workspace::GoBack, + &focus_handle, + window, + cx, + ) + } + }), + ); + + let recent_entries_menu = div().child( + PopoverMenu::new("agent-nav-menu") + .trigger_with_tooltip( + IconButton::new("agent-nav-menu", IconName::MenuAlt) + .icon_size(IconSize::Small) + .style(ui::ButtonStyle::Subtle), + { + let focus_handle = focus_handle.clone(); + move |window, cx| { + Tooltip::for_action_in( + "Toggle Panel Menu", + &ToggleNavigationMenu, + &focus_handle, + window, + cx, + ) + } + }, + ) + .anchor(Corner::TopLeft) + .with_handle(self.assistant_navigation_menu_handle.clone()) + .menu({ + let menu = self.assistant_navigation_menu.clone(); + move |window, cx| { + if let Some(menu) = menu.as_ref() { + menu.update(cx, |_, cx| { + cx.defer_in(window, |menu, window, cx| { + menu.rebuild(window, cx); + }); + }) + } + menu.clone() + } + }), + ); + + let zoom_in_label = if self.is_zoomed(window, cx) { + "Zoom Out" } else { - "Enable Full Screen" + "Zoom In" }; - let selected_agent = self.selected_agent.clone(); + let active_thread = match &self.active_view { + ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()), + ActiveView::ExternalAgentThread { .. } + | ActiveView::TextThread { .. } + | ActiveView::History + | ActiveView::Configuration => None, + }; - PopoverMenu::new("agent-options-menu") + let new_thread_menu = PopoverMenu::new("new_thread_menu") + .trigger_with_tooltip( + IconButton::new("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small), + Tooltip::text("New Thread…"), + ) + .anchor(Corner::TopRight) + .with_handle(self.new_thread_menu_handle.clone()) + .menu({ + let focus_handle = focus_handle.clone(); + move |window, cx| { + let active_thread = active_thread.clone(); + Some(ContextMenu::build(window, cx, |mut menu, _window, cx| { + menu = menu + .context(focus_handle.clone()) + .when(cx.has_flag::(), |this| { + this.header("Zed Agent") + }) + .item( + ContextMenuEntry::new("New Thread") + .icon(IconName::NewThread) + .icon_color(Color::Muted) + .action(NewThread::default().boxed_clone()) + .handler(move |window, cx| { + window.dispatch_action( + NewThread::default().boxed_clone(), + cx, + ); + }), + ) + .item( + ContextMenuEntry::new("New Text Thread") + .icon(IconName::NewTextThread) + .icon_color(Color::Muted) + .action(NewTextThread.boxed_clone()) + .handler(move |window, cx| { + window.dispatch_action(NewTextThread.boxed_clone(), cx); + }), + ) + .when_some(active_thread, |this, active_thread| { + let thread = active_thread.read(cx); + + if !thread.is_empty() { + let thread_id = thread.id().clone(); + this.item( + ContextMenuEntry::new("New From Summary") + .icon(IconName::NewFromSummary) + .icon_color(Color::Muted) + .handler(move |window, cx| { + window.dispatch_action( + Box::new(NewThread { + from_thread_id: Some(thread_id.clone()), + }), + cx, + ); + }), + ) + } else { + this + } + }) + .when(cx.has_flag::(), |this| { + this.separator() + .header("External Agents") + .item( + ContextMenuEntry::new("New Gemini Thread") + .icon(IconName::AiGemini) + .icon_color(Color::Muted) + .handler(move |window, cx| { + window.dispatch_action( + NewExternalAgentThread { + agent: Some(crate::ExternalAgent::Gemini), + } + .boxed_clone(), + cx, + ); + }), + ) + .item( + ContextMenuEntry::new("New Claude Code Thread") + .icon(IconName::AiClaude) + .icon_color(Color::Muted) + .handler(move |window, cx| { + window.dispatch_action( + NewExternalAgentThread { + agent: Some( + crate::ExternalAgent::ClaudeCode, + ), + } + .boxed_clone(), + cx, + ); + }), + ) + .item( + ContextMenuEntry::new("New Codex Thread") + .icon(IconName::AiOpenAi) + .icon_color(Color::Muted) + .handler(move |window, cx| { + window.dispatch_action( + NewExternalAgentThread { + agent: Some(crate::ExternalAgent::Codex), + } + .boxed_clone(), + cx, + ); + }), + ) + }); + menu + })) + } + }); + + let agent_panel_menu = PopoverMenu::new("agent-options-menu") .trigger_with_tooltip( IconButton::new("agent-options-menu", IconName::Ellipsis) .icon_size(IconSize::Small), @@ -2222,6 +2036,7 @@ impl AgentPanel { .anchor(Corner::TopRight) .with_handle(self.agent_panel_menu_handle.clone()) .menu({ + let focus_handle = focus_handle.clone(); move |window, cx| { Some(ContextMenu::build(window, cx, |mut menu, _window, _| { menu = menu.context(focus_handle.clone()); @@ -2279,143 +2094,7 @@ impl AgentPanel { menu = menu .action("Rules…", Box::new(OpenRulesLibrary::default())) .action("Settings", Box::new(OpenSettings)) - .separator() - .action(full_screen_label, Box::new(ToggleZoom)); - - if selected_agent == AgentType::Gemini { - menu = menu.action("Reauthenticate", Box::new(ReauthenticateAgent)) - } - - menu - })) - } - }) - } - - fn render_recent_entries_menu( - &self, - icon: IconName, - corner: Corner, - cx: &mut Context, - ) -> impl IntoElement { - let focus_handle = self.focus_handle(cx); - - PopoverMenu::new("agent-nav-menu") - .trigger_with_tooltip( - IconButton::new("agent-nav-menu", icon).icon_size(IconSize::Small), - { - move |window, cx| { - Tooltip::for_action_in( - "Toggle Recent Threads", - &ToggleNavigationMenu, - &focus_handle, - window, - cx, - ) - } - }, - ) - .anchor(corner) - .with_handle(self.assistant_navigation_menu_handle.clone()) - .menu({ - let menu = self.assistant_navigation_menu.clone(); - move |window, cx| { - telemetry::event!("View Thread History Clicked"); - - if let Some(menu) = menu.as_ref() { - menu.update(cx, |_, cx| { - cx.defer_in(window, |menu, window, cx| { - menu.rebuild(window, cx); - }); - }) - } - menu.clone() - } - }) - } - - fn render_toolbar_back_button(&self, cx: &mut Context) -> impl IntoElement { - let focus_handle = self.focus_handle(cx); - - IconButton::new("go-back", IconName::ArrowLeft) - .icon_size(IconSize::Small) - .on_click(cx.listener(|this, _, window, cx| { - this.go_back(&workspace::GoBack, window, cx); - })) - .tooltip({ - move |window, cx| { - Tooltip::for_action_in("Go Back", &workspace::GoBack, &focus_handle, window, cx) - } - }) - } - - fn render_toolbar_old(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let focus_handle = self.focus_handle(cx); - - let active_thread = match &self.active_view { - ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()), - ActiveView::ExternalAgentThread { .. } - | ActiveView::TextThread { .. } - | ActiveView::History - | ActiveView::Configuration => None, - }; - - let new_thread_menu = PopoverMenu::new("new_thread_menu") - .trigger_with_tooltip( - IconButton::new("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small), - Tooltip::text("New Thread…"), - ) - .anchor(Corner::TopRight) - .with_handle(self.new_thread_menu_handle.clone()) - .menu({ - move |window, cx| { - let active_thread = active_thread.clone(); - Some(ContextMenu::build(window, cx, |mut menu, _window, cx| { - menu = menu - .context(focus_handle.clone()) - .when_some(active_thread, |this, active_thread| { - let thread = active_thread.read(cx); - - if !thread.is_empty() { - let thread_id = thread.id().clone(); - this.item( - ContextMenuEntry::new("New From Summary") - .icon(IconName::ThreadFromSummary) - .icon_color(Color::Muted) - .handler(move |window, cx| { - window.dispatch_action( - Box::new(NewThread { - from_thread_id: Some(thread_id.clone()), - }), - cx, - ); - }), - ) - } else { - this - } - }) - .item( - ContextMenuEntry::new("New Thread") - .icon(IconName::Thread) - .icon_color(Color::Muted) - .action(NewThread::default().boxed_clone()) - .handler(move |window, cx| { - window.dispatch_action( - NewThread::default().boxed_clone(), - cx, - ); - }), - ) - .item( - ContextMenuEntry::new("New Text Thread") - .icon(IconName::TextThread) - .icon_color(Color::Muted) - .action(NewTextThread.boxed_clone()) - .handler(move |window, cx| { - window.dispatch_action(NewTextThread.boxed_clone(), cx); - }), - ); + .action(zoom_in_label, Box::new(ToggleZoom)); menu })) } @@ -2437,13 +2116,8 @@ impl AgentPanel { .pl_1() .gap_1() .child(match &self.active_view { - ActiveView::History | ActiveView::Configuration => div() - .pl(DynamicSpacing::Base04.rems(cx)) - .child(self.render_toolbar_back_button(cx)) - .into_any_element(), - _ => self - .render_recent_entries_menu(IconName::MenuAlt, Corner::TopLeft, cx) - .into_any_element(), + ActiveView::History | ActiveView::Configuration => go_back_button, + _ => recent_entries_menu, }) .child(self.render_title_view(window, cx)), ) @@ -2460,306 +2134,11 @@ impl AgentPanel { .border_l_1() .border_color(cx.theme().colors().border) .child(new_thread_menu) - .child(self.render_panel_options_menu(window, cx)), + .child(agent_panel_menu), ), ) } - fn render_toolbar_new(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let focus_handle = self.focus_handle(cx); - - let active_thread = match &self.active_view { - ActiveView::ExternalAgentThread { thread_view } => { - thread_view.read(cx).as_native_thread(cx) - } - ActiveView::Thread { .. } - | ActiveView::TextThread { .. } - | ActiveView::History - | ActiveView::Configuration => None, - }; - - let new_thread_menu = PopoverMenu::new("new_thread_menu") - .trigger_with_tooltip( - IconButton::new("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small), - { - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "New…", - &ToggleNewThreadMenu, - &focus_handle, - window, - cx, - ) - } - }, - ) - .anchor(Corner::TopLeft) - .with_handle(self.new_thread_menu_handle.clone()) - .menu({ - let workspace = self.workspace.clone(); - - move |window, cx| { - telemetry::event!("New Thread Clicked"); - - let active_thread = active_thread.clone(); - Some(ContextMenu::build(window, cx, |mut menu, _window, cx| { - menu = menu - .context(focus_handle.clone()) - .header("Zed Agent") - .when_some(active_thread, |this, active_thread| { - let thread = active_thread.read(cx); - - if !thread.is_empty() { - let session_id = thread.id().clone(); - this.item( - ContextMenuEntry::new("New From Summary") - .icon(IconName::ThreadFromSummary) - .icon_color(Color::Muted) - .handler(move |window, cx| { - window.dispatch_action( - Box::new(NewNativeAgentThreadFromSummary { - from_session_id: session_id.clone(), - }), - cx, - ); - }), - ) - } else { - this - } - }) - .item( - ContextMenuEntry::new("New Thread") - .action(NewThread::default().boxed_clone()) - .icon(IconName::Thread) - .icon_color(Color::Muted) - .handler({ - let workspace = workspace.clone(); - move |window, cx| { - if let Some(workspace) = workspace.upgrade() { - workspace.update(cx, |workspace, cx| { - if let Some(panel) = - workspace.panel::(cx) - { - panel.update(cx, |panel, cx| { - panel.new_agent_thread( - AgentType::NativeAgent, - window, - cx, - ); - }); - } - }); - } - } - }), - ) - .item( - ContextMenuEntry::new("New Text Thread") - .icon(IconName::TextThread) - .icon_color(Color::Muted) - .action(NewTextThread.boxed_clone()) - .handler({ - let workspace = workspace.clone(); - move |window, cx| { - if let Some(workspace) = workspace.upgrade() { - workspace.update(cx, |workspace, cx| { - if let Some(panel) = - workspace.panel::(cx) - { - panel.update(cx, |panel, cx| { - panel.new_agent_thread( - AgentType::TextThread, - window, - cx, - ); - }); - } - }); - } - } - }), - ) - .separator() - .header("External Agents") - .when(cx.has_flag::(), |menu| { - menu.item( - ContextMenuEntry::new("New Gemini CLI Thread") - .icon(IconName::AiGemini) - .icon_color(Color::Muted) - .handler({ - let workspace = workspace.clone(); - move |window, cx| { - if let Some(workspace) = workspace.upgrade() { - workspace.update(cx, |workspace, cx| { - if let Some(panel) = - workspace.panel::(cx) - { - panel.update(cx, |panel, cx| { - panel.new_agent_thread( - AgentType::Gemini, - window, - cx, - ); - }); - } - }); - } - } - }), - ) - }) - .when(cx.has_flag::(), |menu| { - menu.item( - ContextMenuEntry::new("New Claude Code Thread") - .icon(IconName::AiClaude) - .icon_color(Color::Muted) - .handler({ - let workspace = workspace.clone(); - move |window, cx| { - if let Some(workspace) = workspace.upgrade() { - workspace.update(cx, |workspace, cx| { - if let Some(panel) = - workspace.panel::(cx) - { - panel.update(cx, |panel, cx| { - panel.new_agent_thread( - AgentType::ClaudeCode, - window, - cx, - ); - }); - } - }); - } - } - }), - ) - }) - .when(cx.has_flag::(), |mut menu| { - // Add custom agents from settings - let settings = - agent_servers::AllAgentServersSettings::get_global(cx); - for (agent_name, agent_settings) in &settings.custom { - menu = menu.item( - ContextMenuEntry::new(format!("New {} Thread", agent_name)) - .icon(IconName::Terminal) - .icon_color(Color::Muted) - .handler({ - let workspace = workspace.clone(); - let agent_name = agent_name.clone(); - let agent_settings = agent_settings.clone(); - move |window, cx| { - if let Some(workspace) = workspace.upgrade() { - workspace.update(cx, |workspace, cx| { - if let Some(panel) = - workspace.panel::(cx) - { - panel.update(cx, |panel, cx| { - panel.new_agent_thread( - AgentType::Custom { - name: agent_name - .clone(), - settings: - agent_settings - .clone(), - }, - window, - cx, - ); - }); - } - }); - } - } - }), - ); - } - - menu - }) - .when(cx.has_flag::(), |menu| { - menu.separator().link( - "Add Other Agents", - OpenBrowser { - url: zed_urls::external_agents_docs(cx), - } - .boxed_clone(), - ) - }); - menu - })) - } - }); - - let selected_agent_label = self.selected_agent.label(); - let selected_agent = div() - .id("selected_agent_icon") - .when_some(self.selected_agent.icon(), |this, icon| { - this.px(DynamicSpacing::Base02.rems(cx)) - .child(Icon::new(icon).color(Color::Muted)) - .tooltip(move |window, cx| { - Tooltip::with_meta( - selected_agent_label.clone(), - None, - "Selected Agent", - window, - cx, - ) - }) - }) - .into_any_element(); - - h_flex() - .id("agent-panel-toolbar") - .h(Tab::container_height(cx)) - .max_w_full() - .flex_none() - .justify_between() - .gap_2() - .bg(cx.theme().colors().tab_bar_background) - .border_b_1() - .border_color(cx.theme().colors().border) - .child( - h_flex() - .size_full() - .gap(DynamicSpacing::Base04.rems(cx)) - .pl(DynamicSpacing::Base04.rems(cx)) - .child(match &self.active_view { - ActiveView::History | ActiveView::Configuration => { - self.render_toolbar_back_button(cx).into_any_element() - } - _ => selected_agent.into_any_element(), - }) - .child(self.render_title_view(window, cx)), - ) - .child( - h_flex() - .flex_none() - .gap(DynamicSpacing::Base02.rems(cx)) - .pl(DynamicSpacing::Base04.rems(cx)) - .pr(DynamicSpacing::Base06.rems(cx)) - .child(new_thread_menu) - .child(self.render_recent_entries_menu( - IconName::MenuAltTemp, - Corner::TopRight, - cx, - )) - .child(self.render_panel_options_menu(window, cx)), - ) - } - - fn render_toolbar(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - if cx.has_flag::() - || cx.has_flag::() - { - self.render_toolbar_new(window, cx).into_any_element() - } else { - self.render_toolbar_old(window, cx).into_any_element() - } - } - fn render_token_count(&self, cx: &App) -> Option { match &self.active_view { ActiveView::Thread { @@ -2878,7 +2257,9 @@ impl AgentPanel { } ActiveView::ExternalAgentThread { .. } | ActiveView::History - | ActiveView::Configuration => None, + | ActiveView::Configuration => { + return None; + } } } @@ -2894,7 +2275,7 @@ impl AgentPanel { .thread() .read(cx) .configured_model() - .is_some_and(|model| { + .map_or(false, |model| { model.provider.id() != language_model::ZED_CLOUD_PROVIDER_ID }) { @@ -2905,7 +2286,7 @@ impl AgentPanel { if LanguageModelRegistry::global(cx) .read(cx) .default_model() - .is_some_and(|model| { + .map_or(false, |model| { model.provider.id() != language_model::ZED_CLOUD_PROVIDER_ID }) { @@ -2917,10 +2298,10 @@ impl AgentPanel { | ActiveView::Configuration => return false, } - let plan = self.user_store.read(cx).plan(); + let plan = self.user_store.read(cx).current_plan(); let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some(); - matches!(plan, Some(Plan::ZedFree)) && has_previous_trial + matches!(plan, Some(Plan::Free)) && has_previous_trial } fn should_render_onboarding(&self, cx: &mut Context) -> bool { @@ -2929,19 +2310,10 @@ impl AgentPanel { } match &self.active_view { - ActiveView::History | ActiveView::Configuration => false, - ActiveView::ExternalAgentThread { thread_view, .. } - if thread_view.read(cx).as_native_thread(cx).is_none() => - { - false - } - _ => { - let history_is_empty = if cx.has_flag::() { - self.acp_history_store.read(cx).is_empty(cx) - } else { - self.history_store - .update(cx, |store, cx| store.recent_entries(1, cx).is_empty()) - }; + ActiveView::Thread { .. } | ActiveView::TextThread { .. } => { + let history_is_empty = self + .history_store + .update(cx, |store, cx| store.recent_entries(1, cx).is_empty()); let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx) .providers() @@ -2953,6 +2325,9 @@ impl AgentPanel { history_is_empty || !has_configured_non_zed_providers } + ActiveView::ExternalAgentThread { .. } + | ActiveView::History + | ActiveView::Configuration => false, } } @@ -2980,16 +2355,6 @@ impl AgentPanel { ) } - fn render_backdrop(&self, cx: &mut Context) -> impl IntoElement { - div() - .size_full() - .absolute() - .inset_0() - .bg(cx.theme().colors().panel_background) - .opacity(0.8) - .block_mouse_except_scroll() - } - fn render_trial_end_upsell( &self, _window: &mut Window, @@ -2999,24 +2364,15 @@ impl AgentPanel { return None; } - Some( - v_flex() - .absolute() - .inset_0() - .size_full() - .bg(cx.theme().colors().panel_background) - .opacity(0.85) - .block_mouse_except_scroll() - .child(EndTrialUpsell::new(Arc::new({ - let this = cx.entity(); - move |_, cx| { - this.update(cx, |_this, cx| { - TrialEndUpsell::set_dismissed(true, cx); - cx.notify(); - }); - } - }))), - ) + Some(EndTrialUpsell::new(Arc::new({ + let this = cx.entity(); + move |_, cx| { + this.update(cx, |_this, cx| { + TrialEndUpsell::set_dismissed(true, cx); + cx.notify(); + }); + } + }))) } fn render_empty_state_section_header( @@ -3025,22 +2381,20 @@ impl AgentPanel { action_slot: Option, cx: &mut Context, ) -> impl IntoElement { - div().pl_1().pr_1p5().child( - h_flex() - .mt_2() - .pl_1p5() - .pb_1() - .w_full() - .justify_between() - .border_b_1() - .border_color(cx.theme().colors().border_variant) - .child( - Label::new(label.into()) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .children(action_slot), - ) + h_flex() + .mt_2() + .pl_1p5() + .pb_1() + .w_full() + .justify_between() + .border_b_1() + .border_color(cx.theme().colors().border_variant) + .child( + Label::new(label.into()) + .size(LabelSize::Small) + .color(Color::Muted), + ) + .children(action_slot) } fn render_thread_empty_state( @@ -3146,12 +2500,22 @@ impl AgentPanel { }), ), ) + }) + .when_some(configuration_error.as_ref(), |this, err| { + this.child(self.render_configuration_error( + err, + &focus_handle, + window, + cx, + )) }), ) }) .when(!recent_history.is_empty(), |parent| { + let focus_handle = focus_handle.clone(); parent .overflow_hidden() + .p_1p5() .justify_end() .gap_1() .child( @@ -3179,15 +2543,14 @@ impl AgentPanel { ), ) .child( - v_flex().p_1().pr_1p5().gap_1().children( - recent_history - .into_iter() - .enumerate() - .map(|(index, entry)| { + v_flex() + .gap_1() + .children(recent_history.into_iter().enumerate().map( + |(index, entry)| { // TODO: Add keyboard navigation. let is_hovered = self.hovered_recent_history_item == Some(index); - HistoryEntryElement::new(entry, cx.entity().downgrade()) + HistoryEntryElement::new(entry.clone(), cx.entity().downgrade()) .hovered(is_hovered) .on_hover(cx.listener( move |this, is_hovered, _window, cx| { @@ -3202,82 +2565,176 @@ impl AgentPanel { }, )) .into_any_element() - }), - ), + }, + )), ) - }) - .when_some(configuration_error.as_ref(), |this, err| { - this.child(self.render_configuration_error(false, err, &focus_handle, window, cx)) + .child(self.render_empty_state_section_header("Start", None, cx)) + .child( + v_flex() + .p_1() + .gap_2() + .child( + h_flex() + .w_full() + .gap_2() + .child( + NewThreadButton::new( + "new-thread-btn", + "New Thread", + IconName::NewThread, + ) + .keybinding(KeyBinding::for_action_in( + &NewThread::default(), + &self.focus_handle(cx), + window, + cx, + )) + .on_click( + |window, cx| { + window.dispatch_action( + NewThread::default().boxed_clone(), + cx, + ) + }, + ), + ) + .child( + NewThreadButton::new( + "new-text-thread-btn", + "New Text Thread", + IconName::NewTextThread, + ) + .keybinding(KeyBinding::for_action_in( + &NewTextThread, + &self.focus_handle(cx), + window, + cx, + )) + .on_click( + |window, cx| { + window.dispatch_action(Box::new(NewTextThread), cx) + }, + ), + ), + ) + .when(cx.has_flag::(), |this| { + this.child( + h_flex() + .w_full() + .gap_2() + .child( + NewThreadButton::new( + "new-gemini-thread-btn", + "New Gemini Thread", + IconName::AiGemini, + ) + // .keybinding(KeyBinding::for_action_in( + // &OpenHistory, + // &self.focus_handle(cx), + // window, + // cx, + // )) + .on_click( + |window, cx| { + window.dispatch_action( + Box::new(NewExternalAgentThread { + agent: Some( + crate::ExternalAgent::Gemini, + ), + }), + cx, + ) + }, + ), + ) + .child( + NewThreadButton::new( + "new-claude-thread-btn", + "New Claude Code Thread", + IconName::AiClaude, + ) + // .keybinding(KeyBinding::for_action_in( + // &OpenHistory, + // &self.focus_handle(cx), + // window, + // cx, + // )) + .on_click( + |window, cx| { + window.dispatch_action( + Box::new(NewExternalAgentThread { + agent: Some( + crate::ExternalAgent::ClaudeCode, + ), + }), + cx, + ) + }, + ), + ) + .child( + NewThreadButton::new( + "new-codex-thread-btn", + "New Codex Thread", + IconName::AiOpenAi, + ) + .on_click( + |window, cx| { + window.dispatch_action( + Box::new(NewExternalAgentThread { + agent: Some( + crate::ExternalAgent::Codex, + ), + }), + cx, + ) + }, + ), + ), + ) + }), + ) + .when_some(configuration_error.as_ref(), |this, err| { + this.child(self.render_configuration_error(err, &focus_handle, window, cx)) + }) }) } fn render_configuration_error( &self, - border_bottom: bool, configuration_error: &ConfigurationError, focus_handle: &FocusHandle, window: &mut Window, cx: &mut App, ) -> impl IntoElement { - let zed_provider_configured = AgentSettings::get_global(cx) - .default_model - .as_ref() - .is_some_and(|selection| selection.provider.0.as_str() == "zed.dev"); - - let callout = if zed_provider_configured { - Callout::new() - .icon(IconName::Warning) - .severity(Severity::Warning) - .when(border_bottom, |this| { - this.border_position(ui::BorderPosition::Bottom) - }) - .title("Sign in to continue using Zed as your LLM provider.") - .actions_slot( - Button::new("sign_in", "Sign In") - .style(ButtonStyle::Tinted(ui::TintColor::Warning)) - .label_size(LabelSize::Small) - .on_click({ - let workspace = self.workspace.clone(); - move |_, _, cx| { - let Ok(client) = - workspace.update(cx, |workspace, _| workspace.client().clone()) - else { - return; - }; - - cx.spawn(async move |cx| { - client.sign_in_with_optional_connect(true, cx).await - }) - .detach_and_log_err(cx); - } - }), - ) - } else { - Callout::new() - .icon(IconName::Warning) - .severity(Severity::Warning) - .when(border_bottom, |this| { - this.border_position(ui::BorderPosition::Bottom) - }) - .title(configuration_error.to_string()) - .actions_slot( - Button::new("settings", "Configure") + match configuration_error { + ConfigurationError::ModelNotFound + | ConfigurationError::ProviderNotAuthenticated(_) + | ConfigurationError::NoProvider => Banner::new() + .severity(ui::Severity::Warning) + .child(Label::new(configuration_error.to_string())) + .action_slot( + Button::new("settings", "Configure Provider") .style(ButtonStyle::Tinted(ui::TintColor::Warning)) .label_size(LabelSize::Small) .key_binding( - KeyBinding::for_action_in(&OpenSettings, focus_handle, window, cx) + KeyBinding::for_action_in(&OpenSettings, &focus_handle, window, cx) .map(|kb| kb.size(rems_from_px(12.))), ) .on_click(|_event, window, cx| { window.dispatch_action(OpenSettings.boxed_clone(), cx) }), + ), + ConfigurationError::ProviderPendingTermsAcceptance(provider) => { + Banner::new().severity(ui::Severity::Warning).child( + h_flex().w_full().children( + provider.render_accept_terms( + LanguageModelProviderTosView::ThreadEmptyState, + cx, + ), + ), ) - }; - - match configuration_error { - ConfigurationError::ModelNotFound - | ConfigurationError::ProviderNotAuthenticated(_) - | ConfigurationError::NoProvider => callout.into_any_element(), + } } } @@ -3308,7 +2765,7 @@ impl AgentPanel { let focus_handle = self.focus_handle(cx); let banner = Banner::new() - .severity(Severity::Info) + .severity(ui::Severity::Info) .child(Label::new("Consecutive tool use limit reached.").size(LabelSize::Small)) .action_slot( h_flex() @@ -3419,6 +2876,10 @@ impl AgentPanel { })) } + fn error_callout_bg(&self, cx: &Context) -> Hsla { + cx.theme().status().error.opacity(0.08) + } + fn render_payment_required_error( &self, thread: &Entity, @@ -3427,18 +2888,23 @@ impl AgentPanel { const ERROR_MESSAGE: &str = "You reached your free usage limit. Upgrade to Zed Pro for more prompts."; - Callout::new() - .severity(Severity::Error) - .icon(IconName::XCircle) - .title("Free Usage Exceeded") - .description(ERROR_MESSAGE) - .actions_slot( - h_flex() - .gap_0p5() - .child(self.upgrade_button(thread, cx)) - .child(self.create_copy_button(ERROR_MESSAGE)), + let icon = Icon::new(IconName::XCircle) + .size(IconSize::Small) + .color(Color::Error); + + div() + .border_t_1() + .border_color(cx.theme().colors().border) + .child( + Callout::new() + .icon(icon) + .title("Free Usage Exceeded") + .description(ERROR_MESSAGE) + .tertiary_action(self.upgrade_button(thread, cx)) + .secondary_action(self.create_copy_button(ERROR_MESSAGE)) + .primary_action(self.dismiss_error_button(thread, cx)) + .bg_color(self.error_callout_bg(cx)), ) - .dismiss_action(self.dismiss_error_button(thread, cx)) .into_any_element() } @@ -3450,25 +2916,43 @@ impl AgentPanel { ) -> AnyElement { let error_message = match plan { Plan::ZedPro => "Upgrade to usage-based billing for more prompts.", - Plan::ZedProTrial | Plan::ZedFree => "Upgrade to Zed Pro for more prompts.", + Plan::ZedProTrial | Plan::Free => "Upgrade to Zed Pro for more prompts.", }; - Callout::new() - .severity(Severity::Error) - .title("Model Prompt Limit Reached") - .description(error_message) - .actions_slot( - h_flex() - .gap_0p5() - .child(self.upgrade_button(thread, cx)) - .child(self.create_copy_button(error_message)), + let icon = Icon::new(IconName::XCircle) + .size(IconSize::Small) + .color(Color::Error); + + div() + .border_t_1() + .border_color(cx.theme().colors().border) + .child( + Callout::new() + .icon(icon) + .title("Model Prompt Limit Reached") + .description(error_message) + .tertiary_action(self.upgrade_button(thread, cx)) + .secondary_action(self.create_copy_button(error_message)) + .primary_action(self.dismiss_error_button(thread, cx)) + .bg_color(self.error_callout_bg(cx)), ) - .dismiss_action(self.dismiss_error_button(thread, cx)) .into_any_element() } - fn render_retry_button(&self, thread: &Entity) -> AnyElement { - Button::new("retry", "Retry") + fn render_error_message( + &self, + header: SharedString, + message: SharedString, + thread: &Entity, + cx: &mut Context, + ) -> AnyElement { + let message_with_header = format!("{}\n{}", header, message); + + let icon = Icon::new(IconName::XCircle) + .size(IconSize::Small) + .color(Color::Error); + + let retry_button = Button::new("retry", "Retry") .icon(IconName::RotateCw) .icon_position(IconPosition::Start) .icon_size(IconSize::Small) @@ -3483,31 +2967,21 @@ impl AgentPanel { }); }); } - }) - .into_any_element() - } + }); - fn render_error_message( - &self, - header: SharedString, - message: SharedString, - thread: &Entity, - cx: &mut Context, - ) -> AnyElement { - let message_with_header = format!("{}\n{}", header, message); - - Callout::new() - .severity(Severity::Error) - .icon(IconName::XCircle) - .title(header) - .description(message) - .actions_slot( - h_flex() - .gap_0p5() - .child(self.render_retry_button(thread)) - .child(self.create_copy_button(message_with_header)), + div() + .border_t_1() + .border_color(cx.theme().colors().border) + .child( + Callout::new() + .icon(icon) + .title(header) + .description(message.clone()) + .primary_action(retry_button) + .secondary_action(self.dismiss_error_button(thread, cx)) + .tertiary_action(self.create_copy_button(message_with_header)) + .bg_color(self.error_callout_bg(cx)), ) - .dismiss_action(self.dismiss_error_button(thread, cx)) .into_any_element() } @@ -3516,39 +2990,60 @@ impl AgentPanel { message: SharedString, can_enable_burn_mode: bool, thread: &Entity, + cx: &mut Context, ) -> AnyElement { - Callout::new() - .severity(Severity::Error) + let icon = Icon::new(IconName::XCircle) + .size(IconSize::Small) + .color(Color::Error); + + let retry_button = Button::new("retry", "Retry") + .icon(IconName::RotateCw) + .icon_position(IconPosition::Start) + .icon_size(IconSize::Small) + .label_size(LabelSize::Small) + .on_click({ + let thread = thread.clone(); + move |_, window, cx| { + thread.update(cx, |thread, cx| { + thread.clear_last_error(); + thread.thread().update(cx, |thread, cx| { + thread.retry_last_completion(Some(window.window_handle()), cx); + }); + }); + } + }); + + let mut callout = Callout::new() + .icon(icon) .title("Error") - .description(message) - .actions_slot( - h_flex() - .gap_0p5() - .when(can_enable_burn_mode, |this| { - this.child( - Button::new("enable_burn_retry", "Enable Burn Mode and Retry") - .icon(IconName::ZedBurnMode) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .label_size(LabelSize::Small) - .on_click({ - let thread = thread.clone(); - move |_, window, cx| { - thread.update(cx, |thread, cx| { - thread.clear_last_error(); - thread.thread().update(cx, |thread, cx| { - thread.enable_burn_mode_and_retry( - Some(window.window_handle()), - cx, - ); - }); - }); - } - }), - ) - }) - .child(self.render_retry_button(thread)), - ) + .description(message.clone()) + .bg_color(self.error_callout_bg(cx)) + .primary_action(retry_button); + + if can_enable_burn_mode { + let burn_mode_button = Button::new("enable_burn_retry", "Enable Burn Mode and Retry") + .icon(IconName::ZedBurnMode) + .icon_position(IconPosition::Start) + .icon_size(IconSize::Small) + .label_size(LabelSize::Small) + .on_click({ + let thread = thread.clone(); + move |_, window, cx| { + thread.update(cx, |thread, cx| { + thread.clear_last_error(); + thread.thread().update(cx, |thread, cx| { + thread.enable_burn_mode_and_retry(Some(window.window_handle()), cx); + }); + }); + } + }); + callout = callout.secondary_action(burn_mode_button); + } + + div() + .border_t_1() + .border_color(cx.theme().colors().border) + .child(callout) .into_any_element() } @@ -3627,9 +3122,9 @@ impl AgentPanel { .on_drop(cx.listener(move |this, paths: &ExternalPaths, window, cx| { let tasks = paths .paths() - .iter() + .into_iter() .map(|path| { - Workspace::project_path_for_path(this.project.clone(), path, false, cx) + Workspace::project_path_for_path(this.project.clone(), &path, false, cx) }) .collect::>(); cx.spawn_in(window, async move |this, cx| { @@ -3679,10 +3174,8 @@ impl AgentPanel { .detach(); }); } - ActiveView::ExternalAgentThread { thread_view } => { - thread_view.update(cx, |thread_view, cx| { - thread_view.insert_dragged_files(paths, added_worktrees, window, cx); - }); + ActiveView::ExternalAgentThread { .. } => { + unimplemented!() } ActiveView::TextThread { context_editor, .. } => { context_editor.update(cx, |context_editor, cx| { @@ -3723,10 +3216,9 @@ impl Render for AgentPanel { // - Scrolling in all views works as expected // - Files can be dropped into the panel let content = v_flex() - .relative() - .size_full() - .justify_between() .key_context(self.key_context()) + .justify_between() + .size_full() .on_action(cx.listener(Self::cancel)) .on_action(cx.listener(|this, action: &NewThread, window, cx| { this.new_thread(action, window, cx); @@ -3767,19 +3259,16 @@ impl Render for AgentPanel { } })) .on_action(cx.listener(Self::toggle_burn_mode)) - .on_action(cx.listener(|this, _: &ReauthenticateAgent, window, cx| { - if let Some(thread_view) = this.active_thread_view() { - thread_view.update(cx, |thread_view, cx| thread_view.reauthenticate(window, cx)) - } - })) .child(self.render_toolbar(window, cx)) .children(self.render_onboarding(window, cx)) + .children(self.render_trial_end_upsell(window, cx)) .map(|parent| match &self.active_view { ActiveView::Thread { thread, message_editor, .. } => parent + .relative() .child( if thread.read(cx).is_empty() && !self.should_render_onboarding(cx) { self.render_thread_empty_state(window, cx) @@ -3808,6 +3297,7 @@ impl Render for AgentPanel { message, can_enable_burn_mode, thread, + cx, ), }) .into_any(), @@ -3815,19 +3305,24 @@ impl Render for AgentPanel { }) .child(h_flex().relative().child(message_editor.clone()).when( !LanguageModelRegistry::read_global(cx).has_authenticated_provider(cx), - |this| this.child(self.render_backdrop(cx)), + |this| { + this.child( + div() + .size_full() + .absolute() + .inset_0() + .bg(cx.theme().colors().panel_background) + .opacity(0.8) + .block_mouse_except_scroll(), + ) + }, )) .child(self.render_drag_target(cx)), ActiveView::ExternalAgentThread { thread_view, .. } => parent + .relative() .child(thread_view.clone()) .child(self.render_drag_target(cx)), - ActiveView::History => { - if cx.has_flag::() { - parent.child(self.acp_history.clone()) - } else { - parent.child(self.history.clone()) - } - } + ActiveView::History => parent.child(self.history.clone()), ActiveView::TextThread { context_editor, buffer_search_bar, @@ -3841,13 +3336,16 @@ impl Render for AgentPanel { if !self.should_render_onboarding(cx) && let Some(err) = configuration_error.as_ref() { - this.child(self.render_configuration_error( - true, - err, - &self.focus_handle(cx), - window, - cx, - )) + this.child( + div().bg(cx.theme().colors().editor_background).p_2().child( + self.render_configuration_error( + err, + &self.focus_handle(cx), + window, + cx, + ), + ), + ) } else { this } @@ -3860,8 +3358,7 @@ impl Render for AgentPanel { )) } ActiveView::Configuration => parent.children(self.configuration.clone()), - }) - .children(self.render_trial_end_upsell(window, cx)); + }); match self.active_view.which_font_size_used() { WhichFontSize::AgentFont => { @@ -3906,7 +3403,7 @@ impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist { let text_thread_store = None; let context_store = cx.new(|_| ContextStore::new(project.clone(), None)); assistant.assist( - prompt_editor, + &prompt_editor, self.workspace.clone(), context_store, project, @@ -3989,11 +3486,7 @@ impl AgentPanelDelegate for ConcreteAssistantPanelDelegate { // Wait to create a new context until the workspace is no longer // being updated. cx.defer_in(window, move |panel, window, cx| { - if let Some(thread_view) = panel.active_thread_view() { - thread_view.update(cx, |thread_view, cx| { - thread_view.insert_selections(window, cx); - }); - } else if let Some(message_editor) = panel.active_message_editor() { + if let Some(message_editor) = panel.active_message_editor() { message_editor.update(cx, |message_editor, cx| { message_editor.context_store().update(cx, |store, cx| { let buffer = buffer.read(cx); diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 110c432df3..0800031abe 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -5,6 +5,7 @@ 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; @@ -28,19 +29,17 @@ use std::rc::Rc; use std::sync::Arc; use agent::{Thread, ThreadId}; -use agent_servers::AgentServerSettings; use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection}; use assistant_slash_command::SlashCommandRegistry; -use client::Client; +use client::{Client, DisableAiSettings}; use command_palette_hooks::CommandPaletteFilter; use feature_flags::FeatureFlagAppExt as _; use fs::Fs; -use gpui::{Action, App, Entity, SharedString, actions}; +use gpui::{Action, App, Entity, actions}; use language::LanguageRegistry; use language_model::{ ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, }; -use project::DisableAiSettings; use prompt_store::PromptBuilder; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -64,8 +63,6 @@ actions!( NewTextThread, /// Toggles the context picker interface for adding files, symbols, or other context. ToggleContextPicker, - /// Toggles the menu to create new agent threads. - ToggleNewThreadMenu, /// Toggles the navigation menu for switching between threads and views. ToggleNavigationMenu, /// Toggles the options menu for agent settings and preferences. @@ -129,12 +126,6 @@ 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)] @@ -153,50 +144,21 @@ pub struct NewExternalAgentThread { agent: Option, } -#[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)] +#[derive(Default, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] enum ExternalAgent { #[default] Gemini, ClaudeCode, - NativeAgent, - Custom { - name: SharedString, - settings: AgentServerSettings, - }, + Codex, } impl ExternalAgent { - fn name(&self) -> &'static str { + pub fn server(&self) -> Rc { match self { - Self::NativeAgent => "zed", - Self::Gemini => "gemini-cli", - Self::ClaudeCode => "claude-code", - Self::Custom { .. } => "custom", - } - } - - pub fn server( - &self, - fs: Arc, - history: Entity, - ) -> Rc { - match self { - 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, settings } => Rc::new(agent_servers::CustomAgentServer::new( - name.clone(), - settings, - )), + ExternalAgent::Gemini => Rc::new(agent_servers::Gemini), + ExternalAgent::ClaudeCode => Rc::new(agent_servers::ClaudeCode), + ExternalAgent::Codex => Rc::new(agent_servers::Codex), } } } @@ -272,7 +234,13 @@ pub fn init( client.telemetry().clone(), cx, ); - terminal_inline_assistant::init(fs.clone(), prompt_builder, client.telemetry().clone(), cx); + terminal_inline_assistant::init( + fs.clone(), + prompt_builder.clone(), + client.telemetry().clone(), + cx, + ); + indexed_docs::init(cx); cx.observe_new(move |workspace, window, cx| { ConfigureContextServerModal::register(workspace, language_registry.clone(), window, cx) }) @@ -351,7 +319,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); @@ -418,6 +386,7 @@ fn register_slash_commands(cx: &mut App) { slash_command_registry.register_command(assistant_slash_commands::FetchSlashCommand, true); cx.observe_flag::({ + let slash_command_registry = slash_command_registry.clone(); move |is_enabled, _cx| { if is_enabled { slash_command_registry.register_command( @@ -438,6 +407,12 @@ 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); diff --git a/crates/agent_ui/src/buffer_codegen.rs b/crates/agent_ui/src/buffer_codegen.rs index 04eb41793f..615142b73d 100644 --- a/crates/agent_ui/src/buffer_codegen.rs +++ b/crates/agent_ui/src/buffer_codegen.rs @@ -352,12 +352,12 @@ impl CodegenAlternative { event: &multi_buffer::Event, cx: &mut Context, ) { - 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); + 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); + } } } @@ -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,34 +576,38 @@ 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() - && let Some(non_whitespace_ch_ix) = + if line_indent.is_none() { + if 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); + { + 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 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), + ); } - - 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() { @@ -1024,7 +1028,7 @@ where chunk.push('\n'); } - chunk.push_str(line); + chunk.push_str(&line); } consumed += line.len(); @@ -1129,7 +1133,7 @@ mod tests { ) }); - let chunks_tx = simulate_response_stream(&codegen, cx); + let chunks_tx = simulate_response_stream(codegen.clone(), cx); let mut new_text = concat!( " let mut x = 0;\n", @@ -1196,7 +1200,7 @@ mod tests { ) }); - let chunks_tx = simulate_response_stream(&codegen, cx); + let chunks_tx = simulate_response_stream(codegen.clone(), cx); cx.background_executor.run_until_parked(); @@ -1265,7 +1269,7 @@ mod tests { ) }); - let chunks_tx = simulate_response_stream(&codegen, cx); + let chunks_tx = simulate_response_stream(codegen.clone(), cx); cx.background_executor.run_until_parked(); @@ -1334,7 +1338,7 @@ mod tests { ) }); - let chunks_tx = simulate_response_stream(&codegen, cx); + let chunks_tx = simulate_response_stream(codegen.clone(), cx); let new_text = concat!( "func main() {\n", "\tx := 0\n", @@ -1391,7 +1395,7 @@ mod tests { ) }); - let chunks_tx = simulate_response_stream(&codegen, cx); + let chunks_tx = simulate_response_stream(codegen.clone(), cx); chunks_tx .unbounded_send("let mut x = 0;\nx += 1;".to_string()) .unwrap(); @@ -1473,7 +1477,7 @@ mod tests { } fn simulate_response_stream( - codegen: &Entity, + codegen: Entity, cx: &mut TestAppContext, ) -> mpsc::UnboundedSender { let (chunks_tx, chunks_rx) = mpsc::unbounded(); diff --git a/crates/agent_ui/src/burn_mode_tooltip.rs b/crates/agent_ui/src/burn_mode_tooltip.rs new file mode 100644 index 0000000000..6354c07760 --- /dev/null +++ b/crates/agent_ui/src/burn_mode_tooltip.rs @@ -0,0 +1,61 @@ +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) -> 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) + ) + ) + }) + } +} diff --git a/crates/agent_ui/src/context_picker.rs b/crates/agent_ui/src/context_picker.rs index 405b5ed90b..5cc56b014e 100644 --- a/crates/agent_ui/src/context_picker.rs +++ b/crates/agent_ui/src/context_picker.rs @@ -1,19 +1,18 @@ mod completion_provider; -pub(crate) mod fetch_context_picker; +mod fetch_context_picker; pub(crate) mod file_context_picker; -pub(crate) mod rules_context_picker; -pub(crate) mod symbol_context_picker; -pub(crate) mod thread_context_picker; +mod rules_context_picker; +mod symbol_context_picker; +mod thread_context_picker; use std::ops::Range; use std::path::{Path, PathBuf}; use std::sync::Arc; use anyhow::{Result, anyhow}; -use collections::HashSet; pub use completion_provider::ContextPickerCompletionProvider; use editor::display_map::{Crease, CreaseId, CreaseMetadata, FoldId}; -use editor::{Anchor, Editor, ExcerptId, FoldPlaceholder, ToOffset}; +use editor::{Anchor, AnchorRangeExt as _, Editor, ExcerptId, FoldPlaceholder, ToOffset}; use fetch_context_picker::FetchContextPicker; use file_context_picker::FileContextPicker; use file_context_picker::render_file_context_entry; @@ -46,7 +45,7 @@ use agent::{ }; #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum ContextPickerEntry { +enum ContextPickerEntry { Mode(ContextPickerMode), Action(ContextPickerAction), } @@ -75,7 +74,7 @@ impl ContextPickerEntry { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum ContextPickerMode { +enum ContextPickerMode { File, Symbol, Fetch, @@ -84,7 +83,7 @@ pub(crate) enum ContextPickerMode { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum ContextPickerAction { +enum ContextPickerAction { AddSelections, } @@ -103,7 +102,7 @@ impl ContextPickerAction { pub fn icon(&self) -> IconName { match self { - Self::AddSelections => IconName::Reader, + Self::AddSelections => IconName::Context, } } } @@ -148,8 +147,8 @@ impl ContextPickerMode { match self { Self::File => IconName::File, Self::Symbol => IconName::Code, - Self::Fetch => IconName::ToolWeb, - Self::Thread => IconName::Thread, + Self::Fetch => IconName::Globe, + Self::Thread => IconName::MessageBubbles, Self::Rules => RULES_ICON, } } @@ -228,7 +227,7 @@ impl ContextPicker { } fn build_menu(&mut self, window: &mut Window, cx: &mut Context) -> Entity { - let context_picker = cx.entity(); + let context_picker = cx.entity().clone(); let menu = ContextMenu::build(window, cx, move |menu, _window, cx| { let recent = self.recent_entries(cx); @@ -385,11 +384,12 @@ impl ContextPicker { } pub fn select_first(&mut self, window: &mut Window, cx: &mut Context) { - // Other variants already select their first entry on open automatically - if let ContextPickerState::Default(entity) = &self.mode { - entity.update(cx, |entity, cx| { + match &self.mode { + ContextPickerState::Default(entity) => entity.update(cx, |entity, cx| { entity.select_first(&Default::default(), window, cx) - }) + }), + // Other variants already select their first entry on open automatically + _ => {} } } @@ -531,7 +531,7 @@ impl ContextPicker { return vec![]; }; - recent_context_picker_entries_with_store( + recent_context_picker_entries( context_store, self.thread_store.clone(), self.text_thread_store.clone(), @@ -585,8 +585,7 @@ impl Render for ContextPicker { }) } } - -pub(crate) enum RecentEntry { +enum RecentEntry { File { project_path: ProjectPath, path_prefix: Arc, @@ -594,7 +593,7 @@ pub(crate) enum RecentEntry { Thread(ThreadContextEntry), } -pub(crate) fn available_context_picker_entries( +fn available_context_picker_entries( prompt_store: &Option>, thread_store: &Option>, workspace: &Entity, @@ -609,7 +608,9 @@ pub(crate) fn available_context_picker_entries( .read(cx) .active_item(cx) .and_then(|item| item.downcast::()) - .is_some_and(|editor| editor.update(cx, |editor, cx| editor.has_non_empty_selection(cx))); + .map_or(false, |editor| { + editor.update(cx, |editor, cx| editor.has_non_empty_selection(cx)) + }); if has_selection { entries.push(ContextPickerEntry::Action( ContextPickerAction::AddSelections, @@ -629,56 +630,24 @@ pub(crate) fn available_context_picker_entries( entries } -fn recent_context_picker_entries_with_store( +fn recent_context_picker_entries( context_store: Entity, thread_store: Option>, text_thread_store: Option>, workspace: Entity, exclude_path: Option, cx: &App, -) -> Vec { - let project = workspace.read(cx).project(); - - let mut exclude_paths = context_store.read(cx).file_paths(cx); - exclude_paths.extend(exclude_path); - - let exclude_paths = exclude_paths - .into_iter() - .filter_map(|project_path| project.read(cx).absolute_path(&project_path, cx)) - .collect(); - - let exclude_threads = context_store.read(cx).thread_ids(); - - recent_context_picker_entries( - thread_store, - text_thread_store, - workspace, - &exclude_paths, - exclude_threads, - cx, - ) -} - -pub(crate) fn recent_context_picker_entries( - thread_store: Option>, - text_thread_store: Option>, - workspace: Entity, - exclude_paths: &HashSet, - exclude_threads: &HashSet, - cx: &App, ) -> Vec { let mut recent = Vec::with_capacity(6); + let mut current_files = context_store.read(cx).file_paths(cx); + current_files.extend(exclude_path); let workspace = workspace.read(cx); let project = workspace.project().read(cx); recent.extend( workspace .recent_navigation_history_iter(cx) - .filter(|(_, abs_path)| { - abs_path - .as_ref() - .is_none_or(|path| !exclude_paths.contains(path.as_path())) - }) + .filter(|(path, _)| !current_files.contains(path)) .take(4) .filter_map(|(project_path, _)| { project @@ -690,6 +659,8 @@ pub(crate) fn recent_context_picker_entries( }), ); + let current_threads = context_store.read(cx).thread_ids(); + let active_thread_id = workspace .panel::(cx) .and_then(|panel| Some(panel.read(cx).active_thread(cx)?.read(cx).id())); @@ -701,7 +672,7 @@ pub(crate) fn recent_context_picker_entries( let mut threads = unordered_thread_entries(thread_store, text_thread_store, cx) .filter(|(_, thread)| match thread { ThreadContextEntry::Thread { id, .. } => { - Some(id) != active_thread_id && !exclude_threads.contains(id) + Some(id) != active_thread_id && !current_threads.contains(id) } ThreadContextEntry::Context { .. } => true, }) @@ -739,7 +710,7 @@ fn add_selections_as_context( }) } -pub(crate) fn selection_ranges( +fn selection_ranges( workspace: &Entity, cx: &mut App, ) -> Vec<(Entity, Range)> { @@ -818,8 +789,13 @@ pub fn crease_for_mention( let render_trailer = move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any(); - Crease::inline(range, placeholder, fold_toggle("mention"), render_trailer) - .with_metadata(CreaseMetadata { icon_path, label }) + Crease::inline( + range, + placeholder.clone(), + fold_toggle("mention"), + render_trailer, + ) + .with_metadata(CreaseMetadata { icon_path, label }) } fn render_fold_icon_button( @@ -829,9 +805,42 @@ fn render_fold_icon_button( ) -> Arc, &mut App) -> AnyElement> { Arc::new({ move |fold_id, fold_range, cx| { - let is_in_text_selection = editor - .update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx)) - .unwrap_or_default(); + 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::(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() + }) + }); ButtonLike::new(fold_id) .style(ButtonStyle::Filled) diff --git a/crates/agent_ui/src/context_picker/completion_provider.rs b/crates/agent_ui/src/context_picker/completion_provider.rs index 020d799c79..b377e40b19 100644 --- a/crates/agent_ui/src/context_picker/completion_provider.rs +++ b/crates/agent_ui/src/context_picker/completion_provider.rs @@ -35,7 +35,7 @@ use super::symbol_context_picker::search_symbols; use super::thread_context_picker::{ThreadContextEntry, ThreadMatch, search_threads}; use super::{ ContextPickerAction, ContextPickerEntry, ContextPickerMode, MentionLink, RecentEntry, - available_context_picker_entries, recent_context_picker_entries_with_store, selection_ranges, + available_context_picker_entries, recent_context_picker_entries, selection_ranges, }; use crate::message_editor::ContextCreasesAddon; @@ -79,7 +79,8 @@ fn search( ) -> Task> { match mode { Some(ContextPickerMode::File) => { - let search_files_task = search_files(query, cancellation_flag, &workspace, cx); + let search_files_task = + search_files(query.clone(), cancellation_flag.clone(), &workspace, cx); cx.background_spawn(async move { search_files_task .await @@ -90,7 +91,8 @@ fn search( } Some(ContextPickerMode::Symbol) => { - let search_symbols_task = search_symbols(query, cancellation_flag, &workspace, cx); + let search_symbols_task = + search_symbols(query.clone(), cancellation_flag.clone(), &workspace, cx); cx.background_spawn(async move { search_symbols_task .await @@ -106,8 +108,13 @@ 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, cancellation_flag, thread_store, context_store, cx); + 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 @@ -130,7 +137,8 @@ fn search( Some(ContextPickerMode::Rules) => { if let Some(prompt_store) = prompt_store.as_ref() { - let search_rules_task = search_rules(query, cancellation_flag, prompt_store, cx); + let search_rules_task = + search_rules(query.clone(), cancellation_flag.clone(), prompt_store, cx); cx.background_spawn(async move { search_rules_task .await @@ -188,7 +196,7 @@ fn search( let executor = cx.background_executor().clone(); let search_files_task = - search_files(query.clone(), cancellation_flag, &workspace, cx); + search_files(query.clone(), cancellation_flag.clone(), &workspace, cx); let entries = available_context_picker_entries(&prompt_store, &thread_store, &workspace, cx); @@ -275,7 +283,7 @@ impl ContextPickerCompletionProvider { ) -> Option { match entry { ContextPickerEntry::Mode(mode) => Some(Completion { - replace_range: source_range, + replace_range: source_range.clone(), new_text: format!("@{} ", mode.keyword()), label: CodeLabel::plain(mode.label().to_string(), None), icon_path: Some(mode.icon().path().into()), @@ -322,6 +330,9 @@ 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 { @@ -360,7 +371,7 @@ impl ContextPickerCompletionProvider { line_range.end.row + 1 ) .into(), - IconName::Reader.path().into(), + IconName::Context.path().into(), range, editor.downgrade(), ); @@ -412,7 +423,7 @@ impl ContextPickerCompletionProvider { let icon_for_completion = if recent { IconName::HistoryRerun } else { - IconName::Thread + IconName::MessageBubbles }; let new_text = format!("{} ", MentionLink::for_thread(&thread_entry)); let new_text_len = new_text.len(); @@ -425,12 +436,12 @@ impl ContextPickerCompletionProvider { source: project::CompletionSource::Custom, icon_path: Some(icon_for_completion.path().into()), confirm: Some(confirm_completion_callback( - IconName::Thread.path().into(), + IconName::MessageBubbles.path().into(), thread_entry.title().clone(), excerpt_id, source_range.start, new_text_len - 1, - editor, + editor.clone(), context_store.clone(), move |window, cx| match &thread_entry { ThreadContextEntry::Thread { id, .. } => { @@ -499,7 +510,7 @@ impl ContextPickerCompletionProvider { excerpt_id, source_range.start, new_text_len - 1, - editor, + editor.clone(), context_store.clone(), move |_, cx| { let user_prompt_id = rules.prompt_id; @@ -528,15 +539,15 @@ impl ContextPickerCompletionProvider { 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(IconName::Globe.path().into()), insert_text_mode: None, confirm: Some(confirm_completion_callback( - IconName::ToolWeb.path().into(), + IconName::Globe.path().into(), url_to_fetch.clone(), excerpt_id, source_range.start, new_text_len - 1, - editor, + editor.clone(), context_store.clone(), move |_, cx| { let context_store = context_store.clone(); @@ -693,16 +704,16 @@ impl ContextPickerCompletionProvider { excerpt_id, source_range.start, new_text_len - 1, - editor, + editor.clone(), 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, + symbol.clone(), false, - workspace, + workspace.clone(), context_store.downgrade(), cx, ); @@ -717,11 +728,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(); @@ -776,7 +787,7 @@ impl CompletionProvider for ContextPickerCompletionProvider { .and_then(|b| b.read(cx).file()) .map(|file| ProjectPath::from_file(file.as_ref(), cx)); - let recent_entries = recent_context_picker_entries_with_store( + let recent_entries = recent_context_picker_entries( context_store.clone(), thread_store.clone(), text_thread_store.clone(), @@ -1009,7 +1020,7 @@ impl MentionCompletion { && line .chars() .nth(last_mention_start - 1) - .is_some_and(|c| !c.is_whitespace()) + .map_or(false, |c| !c.is_whitespace()) { return None; } @@ -1151,7 +1162,7 @@ mod tests { impl Focusable for AtMentionEditor { fn focus_handle(&self, cx: &App) -> FocusHandle { - self.0.read(cx).focus_handle(cx) + self.0.read(cx).focus_handle(cx).clone() } } @@ -1469,7 +1480,7 @@ mod tests { let completions = editor.current_completions().expect("Missing completions"); completions .into_iter() - .map(|completion| completion.label.text) + .map(|completion| completion.label.text.to_string()) .collect::>() } diff --git a/crates/agent_ui/src/context_picker/fetch_context_picker.rs b/crates/agent_ui/src/context_picker/fetch_context_picker.rs index dd558b2a1c..8ff68a8365 100644 --- a/crates/agent_ui/src/context_picker/fetch_context_picker.rs +++ b/crates/agent_ui/src/context_picker/fetch_context_picker.rs @@ -226,10 +226,9 @@ impl PickerDelegate for FetchContextPickerDelegate { _window: &mut Window, cx: &mut Context>, ) -> Option { - let added = self - .context_store - .upgrade() - .is_some_and(|context_store| context_store.read(cx).includes_url(&self.url)); + let added = self.context_store.upgrade().map_or(false, |context_store| { + context_store.read(cx).includes_url(&self.url) + }); Some( ListItem::new(ix) diff --git a/crates/agent_ui/src/context_picker/file_context_picker.rs b/crates/agent_ui/src/context_picker/file_context_picker.rs index 6c224caf4c..eaf9ed16d6 100644 --- a/crates/agent_ui/src/context_picker/file_context_picker.rs +++ b/crates/agent_ui/src/context_picker/file_context_picker.rs @@ -239,7 +239,9 @@ pub(crate) fn search_files( PathMatchCandidateSet { snapshot: worktree.snapshot(), - include_ignored: worktree.root_entry().is_some_and(|entry| entry.is_ignored), + include_ignored: worktree + .root_entry() + .map_or(false, |entry| entry.is_ignored), include_root_name: true, candidates: project::Candidates::Entries, } @@ -313,7 +315,7 @@ pub fn render_file_context_entry( context_store: WeakEntity, cx: &App, ) -> Stateful
{ - 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 { @@ -332,7 +334,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)); diff --git a/crates/agent_ui/src/context_picker/rules_context_picker.rs b/crates/agent_ui/src/context_picker/rules_context_picker.rs index f3982f61cb..8ce821cfaa 100644 --- a/crates/agent_ui/src/context_picker/rules_context_picker.rs +++ b/crates/agent_ui/src/context_picker/rules_context_picker.rs @@ -159,7 +159,7 @@ pub fn render_thread_context_entry( context_store: WeakEntity, cx: &mut App, ) -> Div { - let added = context_store.upgrade().is_some_and(|context_store| { + let added = context_store.upgrade().map_or(false, |context_store| { context_store .read(cx) .includes_user_rules(user_rules.prompt_id) diff --git a/crates/agent_ui/src/context_picker/symbol_context_picker.rs b/crates/agent_ui/src/context_picker/symbol_context_picker.rs index b00d4e3693..05e77deece 100644 --- a/crates/agent_ui/src/context_picker/symbol_context_picker.rs +++ b/crates/agent_ui/src/context_picker/symbol_context_picker.rs @@ -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) - .is_some_and(|e| !e.is_ignored) + .map_or(false, |e| !e.is_ignored) }) }) .log_err() diff --git a/crates/agent_ui/src/context_picker/thread_context_picker.rs b/crates/agent_ui/src/context_picker/thread_context_picker.rs index 66654f3d8c..cb2e97a493 100644 --- a/crates/agent_ui/src/context_picker/thread_context_picker.rs +++ b/crates/agent_ui/src/context_picker/thread_context_picker.rs @@ -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?; @@ -236,10 +236,12 @@ pub fn render_thread_context_entry( let is_added = match entry { ThreadContextEntry::Thread { id, .. } => context_store .upgrade() - .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)), + .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) + }) + } }; h_flex() @@ -251,7 +253,7 @@ pub fn render_thread_context_entry( .gap_1p5() .max_w_72() .child( - Icon::new(IconName::Thread) + Icon::new(IconName::MessageBubbles) .size(IconSize::XSmall) .color(Color::Muted), ) @@ -336,7 +338,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::>(); let matches = fuzzy::match_strings( &candidates, diff --git a/crates/agent_ui/src/context_strip.rs b/crates/agent_ui/src/context_strip.rs index d25d7d3544..080ffd2ea0 100644 --- a/crates/agent_ui/src/context_strip.rs +++ b/crates/agent_ui/src/context_strip.rs @@ -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, ) { - if let Some(suggested) = self.suggested_context(cx) - && self.is_suggested_focused(&self.added_contexts(cx)) - { - self.add_suggested_context(&suggested, cx); + if let Some(suggested) = self.suggested_context(cx) { + if 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.context_store.update(cx, |context_store, cx| { - context_store.add_suggested_context(suggested, cx) + context_store.add_suggested_context(&suggested, cx) }); cx.notify(); } @@ -504,7 +504,7 @@ impl Render for ContextStrip { ) .on_click({ Rc::new(cx.listener(move |this, event: &ClickEvent, window, cx| { - if event.click_count() > 1 { + if event.down.click_count > 1 { this.open_context(&context, window, cx); } else { this.focused_index = Some(i); diff --git a/crates/agent_ui/src/inline_assistant.rs b/crates/agent_ui/src/inline_assistant.rs index 13f1234b4d..ffa654d12b 100644 --- a/crates/agent_ui/src/inline_assistant.rs +++ b/crates/agent_ui/src/inline_assistant.rs @@ -16,7 +16,7 @@ use agent::{ }; use agent_settings::AgentSettings; use anyhow::{Context as _, Result}; -use client::telemetry::Telemetry; +use client::{DisableAiSettings, telemetry::Telemetry}; use collections::{HashMap, HashSet, VecDeque, hash_map}; use editor::SelectionEffects; use editor::{ @@ -39,7 +39,7 @@ use language_model::{ }; use multi_buffer::MultiBufferRow; use parking_lot::Mutex; -use project::{CodeAction, DisableAiSettings, LspAction, Project, ProjectTransaction}; +use project::{CodeAction, LspAction, Project, ProjectTransaction}; use prompt_store::{PromptBuilder, PromptStore}; use settings::{Settings, SettingsStore}; use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase}; @@ -72,7 +72,7 @@ pub fn init( let Some(window) = window else { return; }; - let workspace = cx.entity(); + let workspace = cx.entity().clone(); InlineAssistant::update_global(cx, |inline_assistant, cx| { inline_assistant.register_workspace(&workspace, window, cx) }); @@ -162,7 +162,7 @@ impl InlineAssistant { let window = windows[0]; let _ = window.update(cx, |_, window, cx| { editor.update(cx, |editor, cx| { - if editor.has_active_edit_prediction() { + if editor.has_active_inline_completion() { editor.cancel(&Default::default(), window, cx); } }); @@ -182,13 +182,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::(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) + if let Some(editor) = item.upgrade().and_then(|item| item.act_as::(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) + } } } } @@ -231,8 +231,8 @@ impl InlineAssistant { ); if DisableAiSettings::get_global(cx).disable_ai { - // Cancel any active edit predictions - if editor.has_active_edit_prediction() { + // Cancel any active completions + if editor.has_active_inline_completion() { editor.cancel(&Default::default(), window, cx); } } @@ -342,11 +342,13 @@ impl InlineAssistant { ) .await .ok(); - if let Some(answer) = answer - && answer == 0 - { - cx.update(|window, cx| window.dispatch_action(Box::new(OpenSettings), cx)) + if let Some(answer) = answer { + if answer == 0 { + cx.update(|window, cx| { + window.dispatch_action(Box::new(OpenSettings), cx) + }) .ok(); + } } anyhow::Ok(()) }) @@ -433,11 +435,11 @@ impl InlineAssistant { } } - if let Some(prev_selection) = selections.last_mut() - && selection.start <= prev_selection.end - { - prev_selection.end = selection.end; - continue; + if let Some(prev_selection) = selections.last_mut() { + if selection.start <= prev_selection.end { + prev_selection.end = selection.end; + continue; + } } let latest_selection = newest_selection.get_or_insert_with(|| selection.clone()); @@ -524,9 +526,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); @@ -548,7 +550,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(); @@ -647,7 +649,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( @@ -983,13 +985,14 @@ 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() - && decorations + if let Some(decorations) = assist.decorations.as_ref() { + if decorations .prompt_editor .focus_handle(cx) .is_focused(window) - { - return; + { + return; + } } } @@ -1120,7 +1123,7 @@ impl InlineAssistant { if editor_assists .scroll_lock .as_ref() - .is_some_and(|lock| lock.assist_id == assist_id) + .map_or(false, |lock| lock.assist_id == assist_id) { editor_assists.scroll_lock = None; } @@ -1500,18 +1503,20 @@ impl InlineAssistant { window: &mut Window, cx: &mut App, ) -> Option { - if let Some(terminal_panel) = workspace.panel::(cx) - && terminal_panel + if let Some(terminal_panel) = workspace.panel::(cx) { + if terminal_panel .read(cx) .focus_handle(cx) .contains_focused(window, cx) - && let Some(terminal_view) = terminal_panel.read(cx).pane().and_then(|pane| { - pane.read(cx) - .active_item() - .and_then(|t| t.downcast::()) - }) - { - return Some(InlineAssistTarget::Terminal(terminal_view)); + { + if let Some(terminal_view) = terminal_panel.read(cx).pane().and_then(|pane| { + pane.read(cx) + .active_item() + .and_then(|t| t.downcast::()) + }) { + return Some(InlineAssistTarget::Terminal(terminal_view)); + } + } } let context_editor = agent_panel @@ -1532,11 +1537,13 @@ impl InlineAssistant { .and_then(|item| item.act_as::(cx)) { Some(InlineAssistTarget::Editor(workspace_editor)) + } else if let Some(terminal_view) = workspace + .active_item(cx) + .and_then(|item| item.act_as::(cx)) + { + Some(InlineAssistTarget::Terminal(terminal_view)) } else { - workspace - .active_item(cx) - .and_then(|item| item.act_as::(cx)) - .map(InlineAssistTarget::Terminal) + None } } } @@ -1691,7 +1698,7 @@ impl InlineAssist { }), range, codegen: codegen.clone(), - workspace, + workspace: workspace.clone(), _subscriptions: vec![ window.on_focus_in(&prompt_editor_focus_handle, cx, move |_, cx| { InlineAssistant::update_global(cx, |this, cx| { @@ -1734,20 +1741,22 @@ impl InlineAssist { return; }; - 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; + 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::( - assist_id.0, - ); + let id = + NotificationId::composite::( + assist_id.0, + ); - workspace.show_toast(Toast::new(id, error), cx); - }) + workspace.show_toast(Toast::new(id, error), cx); + }) + } + } } if assist.decorations.is_none() { @@ -1812,18 +1821,18 @@ impl CodeActionProvider for AssistantCodeActionProvider { has_diagnostics = true; } if has_diagnostics { - if let Some(symbols_containing_start) = snapshot.symbols_containing(range.start, None) - && 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_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) - && 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)); + 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)); + } } Task::ready(Ok(vec![CodeAction { diff --git a/crates/agent_ui/src/inline_prompt_editor.rs b/crates/agent_ui/src/inline_prompt_editor.rs index a626122769..ade7a5e13d 100644 --- a/crates/agent_ui/src/inline_prompt_editor.rs +++ b/crates/agent_ui/src/inline_prompt_editor.rs @@ -75,7 +75,7 @@ impl Render for PromptEditor { 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(); @@ -345,7 +345,7 @@ impl PromptEditor { let prompt = self.editor.read(cx).text(cx); if self .prompt_history_ix - .is_none_or(|ix| self.prompt_history[ix] != prompt) + .map_or(true, |ix| self.prompt_history[ix] != prompt) { self.prompt_history_ix.take(); self.pending_prompt = prompt; @@ -541,7 +541,7 @@ impl PromptEditor { match &self.mode { PromptEditorMode::Terminal { .. } => vec![ accept, - IconButton::new("confirm", IconName::PlayFilled) + IconButton::new("confirm", IconName::Play) .icon_color(Color::Info) .shape(IconButtonShape::Square) .tooltip(|window, cx| { @@ -1229,27 +1229,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", } } diff --git a/crates/agent_ui/src/language_model_selector.rs b/crates/agent_ui/src/language_model_selector.rs index 3633e533da..7121624c87 100644 --- a/crates/agent_ui/src/language_model_selector.rs +++ b/crates/agent_ui/src/language_model_selector.rs @@ -1,6 +1,5 @@ use std::{cmp::Reverse, sync::Arc}; -use cloud_llm_client::Plan; use collections::{HashSet, IndexMap}; use feature_flags::ZedProFeatureFlag; use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; @@ -11,6 +10,7 @@ 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"; @@ -93,7 +93,7 @@ impl LanguageModelPickerDelegate { let entries = models.entries(); Self { - on_model_changed, + on_model_changed: on_model_changed.clone(), all_models: Arc::new(models), selected_index: Self::get_active_model_index(&entries, get_active_model(cx)), filtered_entries: entries, @@ -104,7 +104,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 +296,7 @@ impl ModelMatcher { pub fn fuzzy_search(&self, query: &str) -> Vec { let mut matches = self.bg_executor.block(match_strings( &self.candidates, - query, + &query, false, true, 100, @@ -514,7 +514,7 @@ impl PickerDelegate for LanguageModelPickerDelegate { .pl_0p5() .gap_1p5() .w(px(240.)) - .child(Label::new(model_info.model.name().0).truncate()), + .child(Label::new(model_info.model.name().0.clone()).truncate()), ) .end_slot(div().pr_3().when(is_selected, |this| { this.child( @@ -536,7 +536,7 @@ impl PickerDelegate for LanguageModelPickerDelegate { ) -> Option { use feature_flags::FeatureFlagAppExt; - let plan = Plan::ZedPro; + let plan = proto::Plan::ZedPro; Some( h_flex() @@ -557,7 +557,7 @@ impl PickerDelegate for LanguageModelPickerDelegate { window .dispatch_action(Box::new(zed_actions::OpenAccountSettings), cx) }), - Plan::ZedFree | Plan::ZedProTrial => Button::new( + Plan::Free | Plan::ZedProTrial => Button::new( "try-pro", if plan == Plan::ZedProTrial { "Upgrade to Pro" diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index 45e7529ec2..e00a0087eb 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -6,7 +6,7 @@ use crate::agent_diff::AgentDiffThread; use crate::agent_model_selector::AgentModelSelector; use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip}; use crate::ui::{ - BurnModeTooltip, + MaxModeTooltip, preview::{AgentPreview, UsageCallout}, }; use agent::history_store::HistoryStore; @@ -14,9 +14,10 @@ use agent::{ context::{AgentContextKey, ContextLoadResult, load_context}, context_store::ContextStoreEvent, }; -use agent_settings::{AgentProfileId, AgentSettings, CompletionMode}; +use agent_settings::{AgentSettings, CompletionMode}; use ai_onboarding::ApiKeysWithProviders; use buffer_diff::BufferDiff; +use client::CloudUserStore; use cloud_llm_client::CompletionIntent; use collections::{HashMap, HashSet}; use editor::actions::{MoveUp, Paste}; @@ -55,7 +56,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::{ProfileProvider, ProfileSelector}; +use crate::profile_selector::ProfileSelector; use crate::{ ActiveThread, AgentDiffPane, ChatWithFollow, ExpandMessageEditor, Follow, KeepAll, ModelUsageContext, NewThread, OpenAgentDiff, RejectAll, RemoveAllContext, ToggleBurnMode, @@ -77,6 +78,7 @@ pub struct MessageEditor { editor: Entity, workspace: WeakEntity, project: Entity, + cloud_user_store: Entity, context_store: Entity, prompt_store: Option>, history_store: Option>, @@ -117,7 +119,7 @@ pub(crate) fn create_editor( let mut editor = Editor::new( editor::EditorMode::AutoHeight { min_lines, - max_lines, + max_lines: max_lines, }, buffer, None, @@ -152,28 +154,11 @@ pub(crate) fn create_editor( editor } -impl ProfileProvider for Entity { - 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, workspace: WeakEntity, + cloud_user_store: Entity, context_store: Entity, prompt_store: Option>, thread_store: WeakEntity, @@ -215,10 +200,9 @@ impl MessageEditor { let subscriptions = vec![ cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event), - cx.subscribe(&editor, |this, _, event: &EditorEvent, cx| { - if event == &EditorEvent::BufferEdited { - this.handle_message_changed(cx) - } + cx.subscribe(&editor, |this, _, event, cx| match event { + EditorEvent::BufferEdited => this.handle_message_changed(cx), + _ => {} }), cx.observe(&context_store, |this, _, cx| { // When context changes, reload it for token counting. @@ -240,15 +224,15 @@ impl MessageEditor { ) }); - let profile_selector = cx.new(|cx| { - ProfileSelector::new(fs, Arc::new(thread.clone()), editor.focus_handle(cx), cx) - }); + let profile_selector = + cx.new(|cx| ProfileSelector::new(fs, thread.clone(), editor.focus_handle(cx), cx)); Self { editor: editor.clone(), project: thread.read(cx).project().clone(), + cloud_user_store, thread, - incompatible_tools_state: incompatible_tools, + incompatible_tools_state: incompatible_tools.clone(), workspace, context_store, prompt_store, @@ -378,13 +362,18 @@ impl MessageEditor { } fn send_to_model(&mut self, window: &mut Window, cx: &mut Context) { - let Some(ConfiguredModel { model, .. }) = self + let Some(ConfiguredModel { model, provider }) = 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); @@ -437,11 +426,11 @@ impl MessageEditor { thread.cancel_editing(cx); }); - let canceled = self.thread.update(cx, |thread, cx| { + let cancelled = self.thread.update(cx, |thread, cx| { thread.cancel_last_completion(Some(window.window_handle()), cx) }); - if canceled { + if cancelled { self.set_editor_is_expanded(false, cx); self.send_to_model(window, cx); } @@ -620,7 +609,7 @@ impl MessageEditor { this.toggle_burn_mode(&ToggleBurnMode, window, cx); })) .tooltip(move |_window, cx| { - cx.new(|_| BurnModeTooltip::new().selected(burn_mode_enabled)) + cx.new(|_| MaxModeTooltip::new().selected(burn_mode_enabled)) .into() }) .into_any_element(), @@ -686,7 +675,11 @@ impl MessageEditor { .as_ref() .map(|model| { self.incompatible_tools_state.update(cx, |state, cx| { - state.incompatible_tools(&model.model, cx).to_vec() + state + .incompatible_tools(&model.model, cx) + .iter() + .cloned() + .collect::>() }) }) .unwrap_or_default(); @@ -736,7 +729,7 @@ impl MessageEditor { .when(focus_handle.is_focused(window), |this| { this.child( IconButton::new("toggle-height", expand_icon) - .icon_size(IconSize::Small) + .icon_size(IconSize::XSmall) .icon_color(Color::Muted) .tooltip({ let focus_handle = focus_handle.clone(); @@ -834,6 +827,7 @@ 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 @@ -841,7 +835,7 @@ impl MessageEditor { parent.child( IconButton::new( "stop-generation", - IconName::Stop, + IconName::StopFilled, ) .icon_color(Color::Error) .style(ButtonStyle::Tinted( @@ -1127,7 +1121,7 @@ impl MessageEditor { ) .when(is_edit_changes_expanded, |parent| { parent.child( - v_flex().children(changed_buffers.iter().enumerate().flat_map( + v_flex().children(changed_buffers.into_iter().enumerate().flat_map( |(index, (buffer, _diff))| { let file = buffer.read(cx).file()?; let path = file.path(); @@ -1157,7 +1151,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(|| { @@ -1284,7 +1278,7 @@ impl MessageEditor { self.thread .read(cx) .configured_model() - .is_some_and(|model| model.provider.id() == ZED_CLOUD_PROVIDER_ID) + .map_or(false, |model| model.provider.id() == ZED_CLOUD_PROVIDER_ID) } fn render_usage_callout(&self, line_height: Pixels, cx: &mut Context) -> Option
{ @@ -1292,14 +1286,16 @@ impl MessageEditor { return None; } - let user_store = self.project.read(cx).user_store().read(cx); - if user_store.is_usage_based_billing_enabled() { + let cloud_user_store = self.cloud_user_store.read(cx); + if cloud_user_store.is_usage_based_billing_enabled() { return None; } - let plan = user_store.plan().unwrap_or(cloud_llm_client::Plan::ZedFree); + let plan = cloud_user_store + .plan() + .unwrap_or(cloud_llm_client::Plan::ZedFree); - let usage = user_store.model_request_usage()?; + let usage = cloud_user_store.model_request_usage()?; Some( div() @@ -1314,10 +1310,14 @@ impl MessageEditor { token_usage_ratio: TokenUsageRatio, cx: &mut Context, ) -> Option
{ - let (icon, severity) = if token_usage_ratio == TokenUsageRatio::Exceeded { - (IconName::Close, Severity::Error) + let icon = if token_usage_ratio == TokenUsageRatio::Exceeded { + Icon::new(IconName::X) + .color(Color::Error) + .size(IconSize::XSmall) } else { - (IconName::Warning, Severity::Warning) + Icon::new(IconName::Warning) + .color(Color::Warning) + .size(IconSize::XSmall) }; let title = if token_usage_ratio == TokenUsageRatio::Exceeded { @@ -1332,34 +1332,30 @@ impl MessageEditor { "To continue, start a new thread from a summary." }; - let callout = Callout::new() + let mut callout = Callout::new() .line_height(line_height) - .severity(severity) .icon(icon) .title(title) .description(description) - .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); - })), - ), + .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); + })), + ); + } + Some( div() .border_t_1() @@ -1395,7 +1391,7 @@ impl MessageEditor { }) .ok(); }); - // Replace existing load task, if any, causing it to be canceled. + // Replace existing load task, if any, causing it to be cancelled. let load_task = load_task.shared(); self.load_context_task = Some(load_task.clone()); cx.spawn(async move |this, cx| { @@ -1437,7 +1433,7 @@ impl MessageEditor { let message_text = editor.read(cx).text(cx); if message_text.is_empty() - && loaded_context.is_none_or(|loaded_context| loaded_context.is_empty()) + && loaded_context.map_or(true, |loaded_context| loaded_context.is_empty()) { return None; } @@ -1550,8 +1546,9 @@ impl ContextCreasesAddon { cx: &mut Context, ) { 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::() else { return; @@ -1571,8 +1568,8 @@ impl ContextCreasesAddon { editor.edit(ranges.into_iter().zip(replacement_texts), cx); cx.notify(); } - }), - ) + }, + )) } pub fn into_inner(self) -> HashMap> { @@ -1600,8 +1597,7 @@ pub fn extract_message_creases( .collect::>(); // Filter the addon's list of creases based on what the editor reports, // since the addon might have removed creases in it. - - editor.display_map.update(cx, |display_map, cx| { + let creases = editor.display_map.update(cx, |display_map, cx| { display_map .snapshot(cx) .crease_snapshot @@ -1625,7 +1621,8 @@ pub fn extract_message_creases( } }) .collect() - }) + }); + creases } impl EventEmitter for MessageEditor {} @@ -1677,7 +1674,7 @@ impl Render for MessageEditor { let has_history = self .history_store .as_ref() - .and_then(|hs| hs.update(cx, |hs, cx| !hs.entries(cx).is_empty()).ok()) + .and_then(|hs| hs.update(cx, |hs, cx| hs.entries(cx).len() > 0).ok()) .unwrap_or(false) || self .thread @@ -1690,7 +1687,7 @@ impl Render for MessageEditor { !has_history && is_signed_out && has_configured_providers, |this| this.child(cx.new(ApiKeysWithProviders::new)), ) - .when(!changed_buffers.is_empty(), |parent| { + .when(changed_buffers.len() > 0, |parent| { parent.child(self.render_edits_bar(&changed_buffers, window, cx)) }) .child(self.render_editor(window, cx)) @@ -1761,6 +1758,7 @@ impl AgentPreview for MessageEditor { ) -> Option { if let Some(workspace) = workspace.upgrade() { let fs = workspace.read(cx).app_state().fs.clone(); + let cloud_user_store = workspace.read(cx).app_state().cloud_user_store.clone(); let project = workspace.read(cx).project().clone(); let weak_project = project.downgrade(); let context_store = cx.new(|_cx| ContextStore::new(weak_project, None)); @@ -1773,6 +1771,7 @@ impl AgentPreview for MessageEditor { MessageEditor::new( fs, workspace.downgrade(), + cloud_user_store, context_store, None, thread_store.downgrade(), @@ -1795,7 +1794,7 @@ impl AgentPreview for MessageEditor { .bg(cx.theme().colors().panel_background) .border_1() .border_color(cx.theme().colors().border) - .child(default_message_editor) + .child(default_message_editor.clone()) .into_any_element(), )]) .into_any_element(), diff --git a/crates/agent_ui/src/profile_selector.rs b/crates/agent_ui/src/profile_selector.rs index f0f53b96b2..ddcb44d46b 100644 --- a/crates/agent_ui/src/profile_selector.rs +++ b/crates/agent_ui/src/profile_selector.rs @@ -1,8 +1,12 @@ use crate::{ManageProfiles, ToggleProfileSelector}; -use agent::agent_profile::{AgentProfile, AvailableProfiles}; +use agent::{ + Thread, + agent_profile::{AgentProfile, AvailableProfiles}, +}; use agent_settings::{AgentDockPosition, AgentProfileId, AgentSettings, builtin_profiles}; use fs::Fs; -use gpui::{Action, Entity, FocusHandle, Subscription, prelude::*}; +use gpui::{Action, Empty, Entity, FocusHandle, Subscription, prelude::*}; +use language_model::LanguageModelRegistry; use settings::{Settings as _, SettingsStore, update_settings_file}; use std::sync::Arc; use ui::{ @@ -10,22 +14,10 @@ use ui::{ 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, - provider: Arc, + thread: Entity, menu_handle: PopoverMenuHandle, focus_handle: FocusHandle, _subscriptions: Vec, @@ -34,7 +26,7 @@ pub struct ProfileSelector { impl ProfileSelector { pub fn new( fs: Arc, - provider: Arc, + thread: Entity, focus_handle: FocusHandle, cx: &mut Context, ) -> Self { @@ -45,7 +37,7 @@ impl ProfileSelector { Self { profiles: AgentProfile::available_profiles(cx), fs, - provider, + thread, menu_handle: PopoverMenuHandle::default(), focus_handle, _subscriptions: vec![settings_subscription], @@ -121,10 +113,10 @@ impl ProfileSelector { builtin_profiles::MINIMAL => Some("Chat about anything with no tools."), _ => None, }; - let thread_profile_id = self.provider.profile_id(cx); + let thread_profile_id = self.thread.read(cx).profile().id(); 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 |_| { @@ -136,16 +128,19 @@ impl ProfileSelector { entry.handler({ let fs = self.fs.clone(); - let provider = self.provider.clone(); + let thread = self.thread.clone(); + let profile_id = profile_id.clone(); move |_window, cx| { update_settings_file::(fs.clone(), cx, { let profile_id = profile_id.clone(); move |settings, _cx| { - settings.set_profile(profile_id); + settings.set_profile(profile_id.clone()); } }); - provider.set_profile(profile_id.clone(), cx); + thread.update(cx, |this, cx| { + this.set_profile(profile_id.clone(), cx); + }); } }) } @@ -154,15 +149,23 @@ impl ProfileSelector { impl Render for ProfileSelector { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let settings = AgentSettings::get_global(cx); - let profile_id = self.provider.profile_id(cx); - let profile = settings.profiles.get(&profile_id); + let profile_id = self.thread.read(cx).profile().id(); + let profile = settings.profiles.get(profile_id); let selected_profile = profile .map(|profile| profile.name.clone()) .unwrap_or_else(|| "Unknown".into()); - if self.provider.profiles_supported(cx) { - let this = cx.entity(); + 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(); let focus_handle = self.focus_handle.clone(); let trigger_button = Button::new("profile-selector-model", selected_profile) .label_size(LabelSize::Small) @@ -174,6 +177,7 @@ impl Render for ProfileSelector { 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", diff --git a/crates/agent_ui/src/slash_command.rs b/crates/agent_ui/src/slash_command.rs index 87e5d45fe8..6b37c5a2d7 100644 --- a/crates/agent_ui/src/slash_command.rs +++ b/crates/agent_ui/src/slash_command.rs @@ -88,6 +88,8 @@ 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, @@ -156,7 +158,7 @@ impl SlashCommandCompletionProvider { if let Some(command) = self.slash_commands.command(command_name, cx) { let completions = command.complete_argument( arguments, - new_cancel_flag, + new_cancel_flag.clone(), self.workspace.clone(), window, cx, diff --git a/crates/agent_ui/src/slash_command_picker.rs b/crates/agent_ui/src/slash_command_picker.rs index a6bb61510c..a757a2f50a 100644 --- a/crates/agent_ui/src/slash_command_picker.rs +++ b/crates/agent_ui/src/slash_command_picker.rs @@ -140,10 +140,12 @@ 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); + } } } } @@ -212,7 +214,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() @@ -304,7 +306,7 @@ where ) .child( Icon::new(IconName::ArrowUpRight) - .size(IconSize::Small) + .size(IconSize::XSmall) .color(Color::Muted), ), ) @@ -327,7 +329,9 @@ where }; let picker_view = cx.new(|cx| { - Picker::uniform_list(delegate, window, cx).max_height(Some(rems(20.).into())) + let picker = + Picker::uniform_list(delegate, window, cx).max_height(Some(rems(20.).into())); + picker }); let handle = self diff --git a/crates/agent_ui/src/slash_command_settings.rs b/crates/agent_ui/src/slash_command_settings.rs index 73e5622aa9..f254d00ec6 100644 --- a/crates/agent_ui/src/slash_command_settings.rs +++ b/crates/agent_ui/src/slash_command_settings.rs @@ -7,11 +7,22 @@ use settings::{Settings, SettingsSources}; /// Settings for slash commands. #[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)] 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 { diff --git a/crates/agent_ui/src/terminal_codegen.rs b/crates/agent_ui/src/terminal_codegen.rs index 5a4a9d560a..54f5b52f58 100644 --- a/crates/agent_ui/src/terminal_codegen.rs +++ b/crates/agent_ui/src/terminal_codegen.rs @@ -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() diff --git a/crates/agent_ui/src/terminal_inline_assistant.rs b/crates/agent_ui/src/terminal_inline_assistant.rs index e7070c0d7f..bcbc308c99 100644 --- a/crates/agent_ui/src/terminal_inline_assistant.rs +++ b/crates/agent_ui/src/terminal_inline_assistant.rs @@ -388,20 +388,20 @@ impl TerminalInlineAssistant { window: &mut Window, cx: &mut App, ) { - 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(); + 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(); + } } } } @@ -432,7 +432,7 @@ impl TerminalInlineAssist { terminal: terminal.downgrade(), prompt_editor: Some(prompt_editor.clone()), codegen: codegen.clone(), - workspace, + workspace: workspace.clone(), context_store, prompt_store, _subscriptions: vec![ @@ -450,20 +450,23 @@ impl TerminalInlineAssist { return; }; - 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; + 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::( - assist_id.0, - ); + let id = + NotificationId::composite::( + assist_id.0, + ); - workspace.show_toast(Toast::new(id, error), cx); - }) + workspace.show_toast(Toast::new(id, error), cx); + }) + } + } } if assist.prompt_editor.is_none() { diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index e9e7eba4b6..3df0a48aa4 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -1,16 +1,18 @@ use crate::{ - QuoteSelection, + burn_mode_tooltip::BurnModeTooltip, 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, FileSlashCommand, selections_creases}; +use assistant_slash_commands::{ + DefaultSlashCommand, DocsSlashCommand, DocsSlashCommandArgs, FileSlashCommand, + selections_creases, +}; use client::{proto, zed_urls}; use collections::{BTreeSet, HashMap, HashSet, hash_map}; use editor::{ - Anchor, Editor, EditorEvent, MenuEditPredictionsPolicy, MultiBuffer, MultiBufferSnapshot, + Anchor, Editor, EditorEvent, MenuInlineCompletionsPolicy, MultiBuffer, MultiBufferSnapshot, RowExt, ToOffset as _, ToPoint, actions::{MoveToEndOfLine, Newline, ShowCompletions}, display_map::{ @@ -28,6 +30,7 @@ use gpui::{ StatefulInteractiveElement, Styled, Subscription, Task, Transformation, WeakEntity, actions, div, img, percentage, point, prelude::*, pulsating_between, size, }; +use indexed_docs::IndexedDocsStore; use language::{ BufferSnapshot, LspAdapterDelegate, ToOffset, language_settings::{SoftWrap, all_language_settings}, @@ -74,7 +77,7 @@ use crate::{slash_command::SlashCommandCompletionProvider, slash_command_picker} use assistant_context::{ AssistantContext, CacheStatus, Content, ContextEvent, ContextId, InvokedSlashCommandId, InvokedSlashCommandStatus, Message, MessageId, MessageMetadata, MessageStatus, - PendingSlashCommandStatus, ThoughtProcessOutputSection, + ParsedSlashCommand, PendingSlashCommandStatus, ThoughtProcessOutputSection, }; actions!( @@ -90,6 +93,8 @@ 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, ] @@ -190,6 +195,7 @@ pub struct TextThreadEditor { invoked_slash_command_creases: HashMap, _subscriptions: Vec, last_error: Option, + show_accept_terms: bool, pub(crate) slash_menu_handle: PopoverMenuHandle>, // dragged_file_worktrees is used to keep references to worktrees that were added @@ -248,7 +254,7 @@ impl TextThreadEditor { editor.set_show_wrap_guides(false, cx); editor.set_show_indent_guides(false, cx); editor.set_completion_provider(Some(Rc::new(completion_provider))); - editor.set_menu_edit_predictions_policy(MenuEditPredictionsPolicy::Never); + editor.set_menu_inline_completions_policy(MenuInlineCompletionsPolicy::Never); editor.set_collaboration_hub(Box::new(project.clone())); let show_edit_predictions = all_language_settings(None, cx) @@ -288,6 +294,7 @@ 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| { @@ -361,12 +368,24 @@ 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) { + 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 @@ -442,7 +461,7 @@ impl TextThreadEditor { || snapshot .chars_at(newest_cursor) .next() - .is_some_and(|ch| ch != '\n') + .map_or(false, |ch| ch != '\n') { editor.move_to_end_of_line( &MoveToEndOfLine { @@ -525,7 +544,7 @@ impl TextThreadEditor { let context = self.context.read(cx); let sections = context .slash_command_output_sections() - .iter() + .into_iter() .filter(|section| section.is_valid(context.buffer().read(cx))) .cloned() .collect::>(); @@ -682,7 +701,19 @@ impl TextThreadEditor { } }; let render_trailer = { - move |_row, _unfold, _window: &mut Window, _cx: &mut App| { + 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, + ); + } + Empty.into_any() } }; @@ -730,27 +761,32 @@ impl TextThreadEditor { ) { if let Some(invoked_slash_command) = self.context.read(cx).invoked_slash_command(&command_id) - && let InvokedSlashCommandStatus::Finished = invoked_slash_command.status { - 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() - }); + if let InvokedSlashCommandStatus::Finished = invoked_slash_command.status { + let run_commands_in_ranges = invoked_slash_command + .run_commands_in_ranges + .iter() + .cloned() + .collect::>(); + 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, + ); + } } } } @@ -1222,7 +1258,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 blocks_to_remove.remove(&message.id).is_some() { + if let Some(_) = blocks_to_remove.remove(&message.id) { // This is an old message that we might modify. let Some((meta, block_id)) = old_blocks.get_mut(&message.id) else { debug_assert!( @@ -1260,7 +1296,7 @@ impl TextThreadEditor { context_editor_view: &Entity, cx: &mut Context, ) -> Option<(String, bool)> { - const CODE_FENCE_DELIMITER: &str = "```"; + const CODE_FENCE_DELIMITER: &'static str = "```"; let context_editor = context_editor_view.read(cx).editor.clone(); context_editor.update(cx, |context_editor, cx| { @@ -1724,7 +1760,7 @@ impl TextThreadEditor { render_slash_command_output_toggle, |_, _, _, _| Empty.into_any(), ) - .with_metadata(metadata.crease) + .with_metadata(metadata.crease.clone()) }), cx, ); @@ -1795,7 +1831,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; + let image = render_image.clone(); anchor.is_valid(&buffer).then(|| BlockProperties { placement: BlockPlacement::Above(anchor), height: Some(MAX_HEIGHT_IN_LINES), @@ -1858,7 +1894,7 @@ impl TextThreadEditor { } fn render_send_button(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let focus_handle = self.focus_handle(cx); + let focus_handle = self.focus_handle(cx).clone(); let (style, tooltip) = match token_state(&self.context, cx) { Some(TokenState::NoTokensLeft { .. }) => ( @@ -1916,6 +1952,7 @@ impl TextThreadEditor { ConfigurationError::NoProvider | ConfigurationError::ModelNotFound | ConfigurationError::ProviderNotAuthenticated(_) => true, + ConfigurationError::ProviderPendingTermsAcceptance(_) => self.show_accept_terms, } } @@ -1999,7 +2036,7 @@ impl TextThreadEditor { None => IconName::Ai, }; - let focus_handle = self.editor().focus_handle(cx); + let focus_handle = self.editor().focus_handle(cx).clone(); PickerPopoverMenu::new( self.language_model_selector.clone(), @@ -2145,8 +2182,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> { - const CODE_BLOCK_NODE: &str = "fenced_code_block"; - const CODE_BLOCK_CONTENT: &str = "code_fence_content"; + const CODE_BLOCK_NODE: &'static str = "fenced_code_block"; + const CODE_BLOCK_CONTENT: &'static str = "code_fence_content"; let layer = snapshot.syntax_layers().next()?; @@ -2196,7 +2233,7 @@ fn render_thought_process_fold_icon_button( let button = match status { ThoughtProcessStatus::Pending => button .child( - Icon::new(IconName::ToolThink) + Icon::new(IconName::LightBulb) .size(IconSize::Small) .color(Color::Muted), ) @@ -2211,7 +2248,7 @@ fn render_thought_process_fold_icon_button( ), ThoughtProcessStatus::Completed => button .style(ButtonStyle::Filled) - .child(Icon::new(IconName::ToolThink).size(IconSize::Small)) + .child(Icon::new(IconName::LightBulb).size(IconSize::Small)) .child(Label::new("Thought Process").single_line()), }; @@ -2361,6 +2398,70 @@ 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, @@ -3113,7 +3214,7 @@ mod tests { let context_editor = window .update(&mut cx, |_, window, cx| { cx.new(|cx| { - TextThreadEditor::for_context( + let editor = TextThreadEditor::for_context( context.clone(), fs, workspace.downgrade(), @@ -3121,7 +3222,8 @@ mod tests { None, window, cx, - ) + ); + editor }) }) .unwrap(); diff --git a/crates/agent_ui/src/thread_history.rs b/crates/agent_ui/src/thread_history.rs index 4ec2078e5d..a2ee816f73 100644 --- a/crates/agent_ui/src/thread_history.rs +++ b/crates/agent_ui/src/thread_history.rs @@ -166,13 +166,14 @@ impl ThreadHistory { this.all_entries.len().saturating_sub(1), cx, ); - } else if let Some(prev_id) = previously_selected_entry - && let Some(new_ix) = this + } else if let Some(prev_id) = previously_selected_entry { + if 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, .. } => { @@ -540,7 +541,6 @@ 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)) @@ -701,7 +701,7 @@ impl RenderOnce for HistoryEntryElement { .on_hover(self.on_hover) .end_slot::(if self.hovered || self.selected { Some( - IconButton::new("delete", IconName::Trash) + IconButton::new("delete", IconName::TrashAlt) .shape(IconButtonShape::Square) .icon_size(IconSize::XSmall) .icon_color(Color::Muted) diff --git a/crates/agent_ui/src/tool_compatibility.rs b/crates/agent_ui/src/tool_compatibility.rs index 046c0a4abc..d4e1da5bb0 100644 --- a/crates/agent_ui/src/tool_compatibility.rs +++ b/crates/agent_ui/src/tool_compatibility.rs @@ -14,11 +14,13 @@ pub struct IncompatibleToolsState { impl IncompatibleToolsState { pub fn new(thread: Entity, cx: &mut Context) -> Self { - let _tool_working_set_subscription = cx.subscribe(&thread, |this, _, event, _| { - if let ThreadEvent::ProfileChanged = event { - this.cache.clear(); - } - }); + let _tool_working_set_subscription = + cx.subscribe(&thread, |this, _, event, _| match event { + ThreadEvent::ProfileChanged => { + this.cache.clear(); + } + _ => {} + }); Self { cache: HashMap::default(), diff --git a/crates/agent_ui/src/ui.rs b/crates/agent_ui/src/ui.rs index 600698b07e..b477a8c385 100644 --- a/crates/agent_ui/src/ui.rs +++ b/crates/agent_ui/src/ui.rs @@ -1,16 +1,14 @@ -mod acp_onboarding_modal; mod agent_notification; mod burn_mode_tooltip; 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 context_pill::*; pub use end_trial_upsell::*; +pub use new_thread_button::*; pub use onboarding_modal::*; -pub use unavailable_editing_tooltip::*; diff --git a/crates/agent_ui/src/ui/acp_onboarding_modal.rs b/crates/agent_ui/src/ui/acp_onboarding_modal.rs deleted file mode 100644 index 0ed9de7221..0000000000 --- a/crates/agent_ui/src/ui/acp_onboarding_modal.rs +++ /dev/null @@ -1,254 +0,0 @@ -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, -} - -impl AcpOnboardingModal { - pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context) { - 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.workspace.update(cx, |workspace, cx| { - workspace.focus_panel::(window, cx); - - if let Some(panel) = workspace.panel::(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) { - 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) { - cx.emit(DismissEvent); - } -} - -impl EventEmitter 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) -> 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( - h_flex() - .gap_4() - .child( - Vector::new(VectorName::AcpLogo, rems_from_px(106.), rems_from_px(40.)) - .color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))), - ) - .child( - Vector::new( - VectorName::AcpLogoSerif, - rems_from_px(111.), - rems_from_px(41.), - ) - .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) - } -} diff --git a/crates/agent_ui/src/ui/burn_mode_tooltip.rs b/crates/agent_ui/src/ui/burn_mode_tooltip.rs index 72faaa614d..97f7853a61 100644 --- a/crates/agent_ui/src/ui/burn_mode_tooltip.rs +++ b/crates/agent_ui/src/ui/burn_mode_tooltip.rs @@ -2,11 +2,11 @@ use crate::ToggleBurnMode; use gpui::{Context, FontWeight, IntoElement, Render, Window}; use ui::{KeyBinding, prelude::*, tooltip_container}; -pub struct BurnModeTooltip { +pub struct MaxModeTooltip { selected: bool, } -impl BurnModeTooltip { +impl MaxModeTooltip { pub fn new() -> Self { Self { selected: false } } @@ -17,7 +17,7 @@ impl BurnModeTooltip { } } -impl Render for BurnModeTooltip { +impl Render for MaxModeTooltip { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let (icon, color) = if self.selected { (IconName::ZedBurnModeOn, Color::Error) diff --git a/crates/agent_ui/src/ui/context_pill.rs b/crates/agent_ui/src/ui/context_pill.rs index 7c7fbd27f0..5dd57de244 100644 --- a/crates/agent_ui/src/ui/context_pill.rs +++ b/crates/agent_ui/src/ui/context_pill.rs @@ -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, cx).into() + ContextPillHover::new_text(text.clone(), 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, + name: title.clone(), 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(), diff --git a/crates/agent_ui/src/ui/end_trial_upsell.rs b/crates/agent_ui/src/ui/end_trial_upsell.rs index 3a8a119800..36770c2197 100644 --- a/crates/agent_ui/src/ui/end_trial_upsell.rs +++ b/crates/agent_ui/src/ui/end_trial_upsell.rs @@ -1,9 +1,9 @@ use std::sync::Arc; -use ai_onboarding::{AgentPanelOnboardingCard, PlanDefinitions}; +use ai_onboarding::{AgentPanelOnboardingCard, BulletItem}; use client::zed_urls; use gpui::{AnyElement, App, IntoElement, RenderOnce, Window}; -use ui::{Divider, Tooltip, prelude::*}; +use ui::{Divider, List, Tooltip, prelude::*}; #[derive(IntoElement, RegisterComponent)] pub struct EndTrialUpsell { @@ -18,8 +18,6 @@ impl EndTrialUpsell { 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 +31,13 @@ impl RenderOnce for EndTrialUpsell { ) .child(Divider::horizontal()), ) - .child(plan_definitions.pro_plan(false)) + .child( + List::new() + .child(BulletItem::new("500 prompts with Claude models")) + .child(BulletItem::new( + "Unlimited edit predictions with Zeta, our open-source model", + )), + ) .child( Button::new("cta-button", "Upgrade to Zed Pro") .full_width() @@ -64,7 +68,11 @@ impl RenderOnce for EndTrialUpsell { ) .child(Divider::horizontal()), ) - .child(plan_definitions.free_plan()); + .child( + List::new() + .child(BulletItem::new("50 prompts with the Claude models")) + .child(BulletItem::new("2,000 accepted edit predictions")), + ); AgentPanelOnboardingCard::new() .child(Headline::new("Your Zed Pro Trial has expired")) @@ -94,20 +102,18 @@ impl RenderOnce for EndTrialUpsell { impl Component for EndTrialUpsell { fn scope() -> ComponentScope { - ComponentScope::Onboarding - } - - fn name() -> &'static str { - "End of Trial Upsell Banner" + ComponentScope::Agent } fn sort_name() -> &'static str { - "End of Trial Upsell Banner" + "AgentEndTrialUpsell" } fn preview(_window: &mut Window, _cx: &mut App) -> Option { Some( v_flex() + .p_4() + .gap_4() .child(EndTrialUpsell { dismiss_upsell: Arc::new(|_, _| {}), }) diff --git a/crates/agent_ui/src/ui/new_thread_button.rs b/crates/agent_ui/src/ui/new_thread_button.rs new file mode 100644 index 0000000000..7764144150 --- /dev/null +++ b/crates/agent_ui/src/ui/new_thread_button.rs @@ -0,0 +1,75 @@ +use gpui::{ClickEvent, ElementId, IntoElement, ParentElement, Styled}; +use ui::prelude::*; + +#[derive(IntoElement)] +pub struct NewThreadButton { + id: ElementId, + label: SharedString, + icon: IconName, + keybinding: Option, + on_click: Option>, +} + +impl NewThreadButton { + pub fn new(id: impl Into, label: impl Into, icon: IconName) -> Self { + Self { + id: id.into(), + label: label.into(), + icon, + keybinding: None, + on_click: None, + } + } + + pub fn keybinding(mut self, keybinding: Option) -> Self { + self.keybinding = keybinding; + self + } + + pub fn on_click(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)) + }) + } +} diff --git a/crates/agent_ui/src/ui/onboarding_modal.rs b/crates/agent_ui/src/ui/onboarding_modal.rs index b8b038bdfc..9e04171ec9 100644 --- a/crates/agent_ui/src/ui/onboarding_modal.rs +++ b/crates/agent_ui/src/ui/onboarding_modal.rs @@ -139,7 +139,7 @@ impl Render for AgentOnboardingModal { .child(Headline::new("Agentic Editing in Zed").size(HeadlineSize::Large)), ) .child(h_flex().absolute().top_2().right_2().child( - IconButton::new("cancel", IconName::Close).on_click(cx.listener( + IconButton::new("cancel", IconName::X).on_click(cx.listener( |_, _: &ClickEvent, _window, cx| { agent_onboarding_event!("Cancelled", trigger = "X click"); cx.emit(DismissEvent); diff --git a/crates/agent_ui/src/ui/preview/usage_callouts.rs b/crates/agent_ui/src/ui/preview/usage_callouts.rs index d4d037b976..64869a6ec7 100644 --- a/crates/agent_ui/src/ui/preview/usage_callouts.rs +++ b/crates/agent_ui/src/ui/preview/usage_callouts.rs @@ -80,24 +80,31 @@ impl RenderOnce for UsageCallout { } }; - let (icon, severity) = if is_limit_reached { - (IconName::Close, Severity::Error) + let icon = if is_limit_reached { + Icon::new(IconName::X) + .color(Color::Error) + .size(IconSize::XSmall) } else { - (IconName::Warning, Severity::Warning) + Icon::new(IconName::Warning) + .color(Color::Warning) + .size(IconSize::XSmall) }; - 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); - }), + 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); + }), + ), ) .into_any_element() } diff --git a/crates/agent_ui/src/ui/unavailable_editing_tooltip.rs b/crates/agent_ui/src/ui/unavailable_editing_tooltip.rs deleted file mode 100644 index 78d4c64e0a..0000000000 --- a/crates/agent_ui/src/ui/unavailable_editing_tooltip.rs +++ /dev/null @@ -1,29 +0,0 @@ -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) -> 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), - ), - ) - }) - } -} diff --git a/crates/ai_onboarding/Cargo.toml b/crates/ai_onboarding/Cargo.toml index 95a45b1a6f..20fd54339e 100644 --- a/crates/ai_onboarding/Cargo.toml +++ b/crates/ai_onboarding/Cargo.toml @@ -20,6 +20,7 @@ cloud_llm_client.workspace = true component.workspace = true gpui.workspace = true language_model.workspace = true +proto.workspace = true serde.workspace = true smallvec.workspace = true telemetry.workspace = true diff --git a/crates/ai_onboarding/src/agent_api_keys_onboarding.rs b/crates/ai_onboarding/src/agent_api_keys_onboarding.rs index fadc4222ae..e86568fe7a 100644 --- a/crates/ai_onboarding/src/agent_api_keys_onboarding.rs +++ b/crates/ai_onboarding/src/agent_api_keys_onboarding.rs @@ -1,6 +1,8 @@ use gpui::{Action, IntoElement, ParentElement, RenderOnce, point}; use language_model::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID}; -use ui::{Divider, List, ListBulletItem, prelude::*}; +use ui::{Divider, List, prelude::*}; + +use crate::BulletItem; pub struct ApiKeysWithProviders { configured_providers: Vec<(IconName, SharedString)>, @@ -11,7 +13,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 +35,7 @@ impl ApiKeysWithProviders { .filter(|provider| { provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID }) - .map(|provider| (provider.icon(), provider.name().0)) + .map(|provider| (provider.icon(), provider.name().0.clone())) .collect() } } @@ -126,7 +128,7 @@ impl RenderOnce for ApiKeysWithoutProviders { ) .child(Divider::horizontal()), ) - .child(List::new().child(ListBulletItem::new( + .child(List::new().child(BulletItem::new( "Add your own keys to use AI without signing in.", ))) .child( diff --git a/crates/ai_onboarding/src/agent_panel_onboarding_content.rs b/crates/ai_onboarding/src/agent_panel_onboarding_content.rs index 77f41d1a73..237b0ae046 100644 --- a/crates/ai_onboarding/src/agent_panel_onboarding_content.rs +++ b/crates/ai_onboarding/src/agent_panel_onboarding_content.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use client::{Client, UserStore}; +use client::{Client, CloudUserStore, UserStore}; use cloud_llm_client::Plan; use gpui::{Entity, IntoElement, ParentElement}; use language_model::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID}; @@ -10,6 +10,7 @@ use crate::{AgentPanelOnboardingCard, ApiKeysWithoutProviders, ZedAiOnboarding}; pub struct AgentPanelOnboarding { user_store: Entity, + cloud_user_store: Entity, client: Arc, configured_providers: Vec<(IconName, SharedString)>, continue_with_zed_ai: Arc, @@ -18,6 +19,7 @@ pub struct AgentPanelOnboarding { impl AgentPanelOnboarding { pub fn new( user_store: Entity, + cloud_user_store: Entity, client: Arc, continue_with_zed_ai: impl Fn(&mut Window, &mut App) + 'static, cx: &mut Context, @@ -25,7 +27,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) @@ -37,6 +39,7 @@ impl AgentPanelOnboarding { Self { user_store, + cloud_user_store, client, configured_providers: Self::compute_available_providers(cx), continue_with_zed_ai: Arc::new(continue_with_zed_ai), @@ -50,15 +53,15 @@ impl AgentPanelOnboarding { .filter(|provider| { provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID }) - .map(|provider| (provider.icon(), provider.name().0)) + .map(|provider| (provider.icon(), provider.name().0.clone())) .collect() } } impl Render for AgentPanelOnboarding { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> 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.cloud_user_store.read(cx).plan() == Some(Plan::ZedProTrial); + let is_pro_user = self.cloud_user_store.read(cx).plan() == Some(Plan::ZedPro); AgentPanelOnboardingCard::new() .child( @@ -74,7 +77,7 @@ impl Render for AgentPanelOnboarding { }), ) .map(|this| { - if enrolled_in_trial || is_pro_user || !self.configured_providers.is_empty() { + if enrolled_in_trial || is_pro_user || self.configured_providers.len() >= 1 { this } else { this.child(ApiKeysWithoutProviders::new()) diff --git a/crates/ai_onboarding/src/ai_onboarding.rs b/crates/ai_onboarding/src/ai_onboarding.rs index 6d8ac64725..3aec9c62cd 100644 --- a/crates/ai_onboarding/src/ai_onboarding.rs +++ b/crates/ai_onboarding/src/ai_onboarding.rs @@ -3,23 +3,58 @@ mod agent_panel_onboarding_card; mod agent_panel_onboarding_content; mod ai_upsell_card; mod edit_prediction_onboarding_content; -mod plan_definitions; mod young_account_banner; pub use agent_api_keys_onboarding::{ApiKeysWithProviders, ApiKeysWithoutProviders}; 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; pub use edit_prediction_onboarding_content::EditPredictionOnboarding; -pub use plan_definitions::PlanDefinitions; pub use young_account_banner::YoungAccountBanner; use std::sync::Arc; use client::{Client, UserStore, zed_urls}; -use gpui::{AnyElement, Entity, IntoElement, ParentElement}; -use ui::{Divider, RegisterComponent, Tooltip, prelude::*}; +use gpui::{AnyElement, Entity, IntoElement, ParentElement, SharedString}; +use ui::{Divider, List, ListItem, RegisterComponent, TintColor, Tooltip, prelude::*}; + +#[derive(IntoElement)] +pub struct BulletItem { + label: SharedString, +} + +impl BulletItem { + pub fn new(label: impl Into) -> Self { + Self { + label: label.into(), + } + } +} + +impl RenderOnce for BulletItem { + fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement { + let line_height = 0.85 * window.line_height(); + + ListItem::new("list-item") + .selectable(false) + .child( + h_flex() + .w_full() + .min_w_0() + .gap_1() + .items_start() + .child( + h_flex().h(line_height).justify_center().child( + Icon::new(IconName::Dash) + .size(IconSize::XSmall) + .color(Color::Hidden), + ), + ) + .child(div().w_full().min_w_0().child(Label::new(self.label))), + ) + .into_any_element() + } +} #[derive(PartialEq)] pub enum SignInStatus { @@ -43,10 +78,12 @@ impl From for SignInStatus { #[derive(RegisterComponent, IntoElement)] pub struct ZedAiOnboarding { pub sign_in_status: SignInStatus, - pub plan: Option, + pub has_accepted_terms_of_service: bool, + pub plan: Option, pub account_too_young: bool, pub continue_with_zed_ai: Arc, pub sign_in: Arc, + pub accept_terms_of_service: Arc, pub dismiss_onboarding: Option>, } @@ -62,15 +99,25 @@ impl ZedAiOnboarding { Self { sign_in_status: status.into(), - plan: store.plan(), + has_accepted_terms_of_service: store.current_user_has_accepted_terms().unwrap_or(false), + plan: store.current_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(); - async move |cx| client.sign_in_with_optional_connect(true, cx).await + async move |cx| { + client.authenticate_and_connect(true, cx).await; + } }) - .detach_and_log_err(cx); + .detach(); }), dismiss_onboarding: None, } @@ -84,9 +131,145 @@ impl ZedAiOnboarding { self } + fn free_plan_definition(&self, cx: &mut App) -> impl IntoElement { + v_flex() + .mt_2() + .gap_1() + .child( + h_flex() + .gap_2() + .child( + Label::new("Free") + .size(LabelSize::Small) + .color(Color::Muted) + .buffer_font(cx), + ) + .child( + Label::new("(Current Plan)") + .size(LabelSize::Small) + .color(Color::Custom(cx.theme().colors().text_muted.opacity(0.6))) + .buffer_font(cx), + ) + .child(Divider::horizontal()), + ) + .child( + List::new() + .child(BulletItem::new("50 prompts per month with Claude models")) + .child(BulletItem::new( + "2,000 accepted edit predictions with Zeta, our open-source model", + )), + ) + } + + fn pro_trial_definition(&self) -> impl IntoElement { + List::new() + .child(BulletItem::new("150 prompts with Claude models")) + .child(BulletItem::new( + "Unlimited accepted edit predictions with Zeta, our open-source model", + )) + } + + fn pro_plan_definition(&self, cx: &mut App) -> impl IntoElement { + v_flex().mt_2().gap_1().map(|this| { + if self.account_too_young { + this.child( + h_flex() + .gap_2() + .child( + Label::new("Pro") + .size(LabelSize::Small) + .color(Color::Accent) + .buffer_font(cx), + ) + .child(Divider::horizontal()), + ) + .child( + List::new() + .child(BulletItem::new("500 prompts per month with Claude models")) + .child(BulletItem::new( + "Unlimited accepted edit predictions with Zeta, our open-source model", + )) + .child(BulletItem::new("$20 USD per month")), + ) + .child( + Button::new("pro", "Get Started") + .full_width() + .style(ButtonStyle::Tinted(ui::TintColor::Accent)) + .on_click(move |_, _window, cx| { + telemetry::event!("Upgrade To Pro Clicked", state = "young-account"); + cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx)) + }), + ) + } else { + this.child( + h_flex() + .gap_2() + .child( + Label::new("Pro Trial") + .size(LabelSize::Small) + .color(Color::Accent) + .buffer_font(cx), + ) + .child(Divider::horizontal()), + ) + .child( + List::new() + .child(self.pro_trial_definition()) + .child(BulletItem::new( + "Try it out for 14 days for free, no credit card required", + )), + ) + .child( + Button::new("pro", "Start Free Trial") + .full_width() + .style(ButtonStyle::Tinted(ui::TintColor::Accent)) + .on_click(move |_, _window, cx| { + telemetry::event!("Start Trial Clicked", state = "post-sign-in"); + cx.open_url(&zed_urls::start_trial_url(cx)) + }), + ) + } + }) + } + + 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::XSmall) + .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 { let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn); - let plan_definitions = PlanDefinitions; v_flex() .gap_1() @@ -96,7 +279,7 @@ impl ZedAiOnboarding { .color(Color::Muted) .mb_2(), ) - .child(plan_definitions.pro_plan(false)) + .child(self.pro_trial_definition()) .child( Button::new("sign_in", "Try Zed Pro for Free") .disabled(signing_in) @@ -115,132 +298,43 @@ impl ZedAiOnboarding { fn render_free_plan_state(&self, cx: &mut App) -> AnyElement { let young_account_banner = YoungAccountBanner; - let plan_definitions = PlanDefinitions; - 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( - v_flex() - .mt_2() - .gap_1() - .child( - h_flex() - .gap_2() - .child( - Label::new("Pro") - .size(LabelSize::Small) - .color(Color::Accent) - .buffer_font(cx), - ) - .child(Divider::horizontal()), - ) - .child(plan_definitions.pro_plan(true)) - .child( - Button::new("pro", "Get Started") - .full_width() - .style(ButtonStyle::Tinted(ui::TintColor::Accent)) - .on_click(move |_, _window, cx| { - telemetry::event!( - "Upgrade To Pro Clicked", - state = "young-account" - ); - cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx)) - }), - ), - ) - .into_any_element() - } else { - v_flex() - .relative() - .gap_1() - .child(Headline::new("Welcome to Zed AI")) - .child( - v_flex() - .mt_2() - .gap_1() - .child( - h_flex() - .gap_2() - .child( - Label::new("Free") - .size(LabelSize::Small) - .color(Color::Muted) - .buffer_font(cx), - ) - .child( - Label::new("(Current Plan)") - .size(LabelSize::Small) - .color(Color::Custom( - cx.theme().colors().text_muted.opacity(0.6), - )) - .buffer_font(cx), - ) - .child(Divider::horizontal()), - ) - .child(plan_definitions.free_plan()), - ) - .when_some( - self.dismiss_onboarding.as_ref(), - |this, dismiss_callback| { - let callback = dismiss_callback.clone(); + v_flex() + .relative() + .gap_1() + .child(Headline::new("Welcome to Zed AI")) + .map(|this| { + if self.account_too_young { + this.child(young_account_banner) + } else { + this.child(self.free_plan_definition(cx)).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) - }), - ), - ) - }, - ) - .child( - v_flex() - .mt_2() - .gap_1() - .child( - h_flex() - .gap_2() - .child( - Label::new("Pro Trial") - .size(LabelSize::Small) - .color(Color::Accent) - .buffer_font(cx), - ) - .child(Divider::horizontal()), - ) - .child(plan_definitions.pro_trial(true)) - .child( - Button::new("pro", "Start Free Trial") - .full_width() - .style(ButtonStyle::Tinted(ui::TintColor::Accent)) - .on_click(move |_, _window, cx| { - telemetry::event!( - "Start Trial Clicked", - state = "post-sign-in" - ); - cx.open_url(&zed_urls::start_trial_url(cx)) - }), - ), - ) - .into_any_element() - } + 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) + }), + ), + ) + }, + ) + } + }) + .child(self.pro_plan_definition(cx)) + .into_any_element() } fn render_trial_state(&self, _cx: &mut App) -> AnyElement { - let plan_definitions = PlanDefinitions; - v_flex() .relative() .gap_1() @@ -250,7 +344,13 @@ impl ZedAiOnboarding { .color(Color::Muted) .mb_2(), ) - .child(plan_definitions.pro_trial(false)) + .child( + List::new() + .child(BulletItem::new("150 prompts with Claude models")) + .child(BulletItem::new( + "Unlimited edit predictions with Zeta, our open-source model", + )), + ) .when_some( self.dismiss_onboarding.as_ref(), |this, dismiss_callback| { @@ -275,8 +375,6 @@ impl ZedAiOnboarding { } fn render_pro_plan_state(&self, _cx: &mut App) -> AnyElement { - let plan_definitions = PlanDefinitions; - v_flex() .gap_1() .child(Headline::new("Welcome to Zed Pro")) @@ -285,26 +383,24 @@ impl ZedAiOnboarding { .color(Color::Muted) .mb_2(), ) - .child(plan_definitions.pro_plan(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) - }), - ), - ) - }, + .child( + List::new() + .child(BulletItem::new("500 prompts with Claude models")) + .child(BulletItem::new( + "Unlimited edit predictions with Zeta, our open-source model", + )), + ) + .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) + } + }), ) .into_any_element() } @@ -313,10 +409,14 @@ 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) { - 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), + if self.has_accepted_terms_of_service { + match self.plan { + None | Some(proto::Plan::Free) => self.render_free_plan_state(cx), + Some(proto::Plan::ZedProTrial) => self.render_trial_state(cx), + Some(proto::Plan::ZedPro) => self.render_pro_plan_state(cx), + } + } else { + self.render_accept_terms_of_service() } } else { self.render_sign_in_disclaimer(cx) @@ -326,29 +426,24 @@ impl RenderOnce for ZedAiOnboarding { impl Component for ZedAiOnboarding { fn scope() -> ComponentScope { - ComponentScope::Onboarding - } - - fn name() -> &'static str { - "Agent Panel Banners" - } - - fn sort_name() -> &'static str { - "Agent Panel Banners" + ComponentScope::Agent } fn preview(_window: &mut Window, _cx: &mut App) -> Option { fn onboarding( sign_in_status: SignInStatus, - plan: Option, + has_accepted_terms_of_service: bool, + plan: Option, 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() @@ -356,29 +451,42 @@ impl Component for ZedAiOnboarding { Some( v_flex() + .p_4() .gap_4() - .items_center() - .max_w_4_5() .children(vec![ single_example( "Not Signed-in", - onboarding(SignInStatus::SignedOut, None, false), + onboarding(SignInStatus::SignedOut, false, None, false), ), single_example( - "Young Account", - onboarding(SignInStatus::SignedIn, None, true), + "Not Accepted ToS", + onboarding(SignInStatus::SignedIn, false, None, false), + ), + single_example( + "Account too young", + onboarding(SignInStatus::SignedIn, false, None, true), ), single_example( "Free Plan", - onboarding(SignInStatus::SignedIn, Some(Plan::ZedFree), false), + onboarding(SignInStatus::SignedIn, true, Some(proto::Plan::Free), false), ), single_example( "Pro Trial", - onboarding(SignInStatus::SignedIn, Some(Plan::ZedProTrial), false), + onboarding( + SignInStatus::SignedIn, + true, + Some(proto::Plan::ZedProTrial), + false, + ), ), single_example( "Pro Plan", - onboarding(SignInStatus::SignedIn, Some(Plan::ZedPro), false), + onboarding( + SignInStatus::SignedIn, + true, + Some(proto::Plan::ZedPro), + false, + ), ), ]) .into_any_element(), diff --git a/crates/ai_onboarding/src/ai_upsell_card.rs b/crates/ai_onboarding/src/ai_upsell_card.rs index 106dcb0aef..041e0d87ec 100644 --- a/crates/ai_onboarding/src/ai_upsell_card.rs +++ b/crates/ai_onboarding/src/ai_upsell_card.rs @@ -1,62 +1,39 @@ -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 client::{Client, zed_urls}; +use gpui::{AnyElement, App, IntoElement, RenderOnce, Window}; +use ui::{Divider, List, Vector, VectorName, prelude::*}; -use crate::{SignInStatus, YoungAccountBanner, plan_definitions::PlanDefinitions}; +use crate::{BulletItem, SignInStatus}; #[derive(IntoElement, RegisterComponent)] pub struct AiUpsellCard { - sign_in_status: SignInStatus, - sign_in: Arc, - account_too_young: bool, - user_plan: Option, - tab_index: Option, + pub sign_in_status: SignInStatus, + pub sign_in: Arc, } impl AiUpsellCard { - pub fn new( - client: Arc, - user_store: &Entity, - user_plan: Option, - cx: &mut App, - ) -> Self { + pub fn new(client: Arc) -> Self { let status = *client.status().borrow(); - let store = user_store.read(cx); Self { - user_plan, sign_in_status: status.into(), sign_in: Arc::new(move |_window, cx| { cx.spawn({ let client = client.clone(); - async move |cx| client.sign_in_with_optional_connect(true, cx).await + async move |cx| { + client.authenticate_and_connect(true, cx).await; + } }) - .detach_and_log_err(cx); + .detach(); }), - account_too_young: store.account_too_young(), - tab_index: None, } } - - pub fn tab_index(mut self, tab_index: Option) -> 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 pro_section = v_flex() - .flex_grow() .w_full() .gap_1() .child( @@ -70,10 +47,15 @@ impl RenderOnce for AiUpsellCard { ) .child(Divider::horizontal()), ) - .child(plan_definitions.pro_plan(false)); + .child( + List::new() + .child(BulletItem::new("500 prompts with Claude models")) + .child(BulletItem::new( + "Unlimited edit predictions with Zeta, our open-source model", + )), + ); let free_section = v_flex() - .flex_grow() .w_full() .gap_1() .child( @@ -87,7 +69,11 @@ impl RenderOnce for AiUpsellCard { ) .child(Divider::horizontal()), ) - .child(plan_definitions.free_plan()); + .child( + List::new() + .child(BulletItem::new("50 prompts with the Claude models")) + .child(BulletItem::new("2,000 accepted edit predictions")), + ); 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.)) @@ -110,181 +96,68 @@ impl RenderOnce for AiUpsellCard { ), )); - let description = PlanDefinitions::AI_DESCRIPTION; + const DESCRIPTION: &str = "Zed offers a complete agentic experience, with robust editing and reviewing features to collaborate with AI."; - let card = v_flex() + let footer_buttons = match self.sign_in_status { + SignInStatus::SignedIn => v_flex() + .items_center() + .gap_1() + .child( + Button::new("sign_in", "Start 14-day Free Pro Trial") + .full_width() + .style(ButtonStyle::Tinted(ui::TintColor::Accent)) + .on_click(move |_, _window, cx| { + telemetry::event!("Start Trial Clicked", state = "post-sign-in"); + cx.open_url(&zed_urls::start_trial_url(cx)) + }), + ) + .child( + Label::new("No credit card required") + .size(LabelSize::Small) + .color(Color::Muted), + ) + .into_any_element(), + _ => Button::new("sign_in", "Sign In") + .full_width() + .style(ButtonStyle::Tinted(ui::TintColor::Accent)) + .on_click({ + let callback = self.sign_in.clone(); + move |_, window, cx| { + telemetry::event!("Start Trial Clicked", state = "pre-sign-in"); + callback(window, cx) + } + }) + .into_any_element(), + }; + + v_flex() .relative() - .flex_grow() - .p_4() - .pt_3() + .p_6() + .pt_4() .border_1() .border_color(cx.theme().colors().border) .rounded_lg() .overflow_hidden() .child(grid_bg) - .child(gradient_bg); - - let plans_section = h_flex() - .w_full() - .mt_1p5() - .mb_2p5() - .items_start() - .gap_6() - .child(free_section) - .child(pro_section); - - let footer_container = v_flex().items_center().gap_1(); - - let certified_user_stamp = div() - .absolute() - .top_2() - .right_2() - .size(rems_from_px(72.)) + .child(gradient_bg) + .child(Headline::new("Try Zed AI")) + .child(Label::new(DESCRIPTION).color(Color::Muted).mb_2()) .child( - Vector::new( - VectorName::ProUserStamp, - rems_from_px(72.), - 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))), - ), - ); - - let pro_trial_stamp = div() - .absolute() - .top_2() - .right_2() - .size(rems_from_px(72.)) - .child( - Vector::new( - VectorName::ProTrialStamp, - rems_from_px(72.), - rems_from_px(72.), - ) - .color(Color::Custom(cx.theme().colors().text.alpha(0.2))), - ); - - match self.sign_in_status { - SignInStatus::SignedIn => match self.user_plan { - None | Some(Plan::ZedFree) => card - .child(Label::new("Try Zed AI").size(LabelSize::Large)) - .map(|this| { - if self.account_too_young { - this.child(young_account_banner).child( - v_flex() - .mt_2() - .gap_1() - .child( - h_flex() - .gap_2() - .child( - Label::new("Pro") - .size(LabelSize::Small) - .color(Color::Accent) - .buffer_font(cx), - ) - .child(Divider::horizontal()), - ) - .child(plan_definitions.pro_plan(true)) - .child( - Button::new("pro", "Get Started") - .full_width() - .style(ButtonStyle::Tinted(ui::TintColor::Accent)) - .on_click(move |_, _window, cx| { - telemetry::event!( - "Upgrade To Pro Clicked", - state = "young-account" - ); - cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx)) - }), - ), - ) - } else { - this.child( - div() - .max_w_3_4() - .mb_2() - .child(Label::new(description).color(Color::Muted)), - ) - .child(plans_section) - .child( - footer_container - .child( - Button::new("start_trial", "Start 14-day Free Pro Trial") - .full_width() - .style(ButtonStyle::Tinted(ui::TintColor::Accent)) - .when_some(self.tab_index, |this, tab_index| { - this.tab_index(tab_index) - }) - .on_click(move |_, _window, cx| { - telemetry::event!( - "Start Trial Clicked", - state = "post-sign-in" - ); - cx.open_url(&zed_urls::start_trial_url(cx)) - }), - ) - .child( - Label::new("No credit card required") - .size(LabelSize::Small) - .color(Color::Muted), - ), - ) - } - }), - 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 - .child(certified_user_stamp) - .child(Label::new("You're in the Zed Pro plan").size(LabelSize::Large)) - .child( - Label::new("Here's what you get:") - .color(Color::Muted) - .mb_2(), - ) - .child(plan_definitions.pro_plan(false)), - }, - // Signed Out State - _ => card - .child(Label::new("Try Zed AI").size(LabelSize::Large)) - .child( - div() - .max_w_3_4() - .mb_2() - .child(Label::new(description).color(Color::Muted)), - ) - .child(plans_section) - .child( - Button::new("sign_in", "Sign In") - .full_width() - .style(ButtonStyle::Tinted(ui::TintColor::Accent)) - .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)) - .on_click({ - let callback = self.sign_in.clone(); - move |_, window, cx| { - telemetry::event!("Start Trial Clicked", state = "pre-sign-in"); - callback(window, cx) - } - }), - ), - } + h_flex() + .mt_1p5() + .mb_2p5() + .items_start() + .gap_12() + .child(free_section) + .child(pro_section), + ) + .child(footer_buttons) } } impl Component for AiUpsellCard { fn scope() -> ComponentScope { - ComponentScope::Onboarding + ComponentScope::Agent } fn name() -> &'static str { @@ -302,69 +175,26 @@ impl Component for AiUpsellCard { fn preview(_window: &mut Window, _cx: &mut App) -> Option { Some( v_flex() + .p_4() .gap_4() - .items_center() - .max_w_4_5() - .child(single_example( - "Signed Out State", - AiUpsellCard { - sign_in_status: SignInStatus::SignedOut, - sign_in: Arc::new(|_, _| {}), - account_too_young: false, - user_plan: None, - tab_index: Some(0), - } - .into_any_element(), - )) - .child(example_group_with_title( - "Signed In States", - vec![ - single_example( - "Free Plan", - AiUpsellCard { - sign_in_status: SignInStatus::SignedIn, - sign_in: Arc::new(|_, _| {}), - account_too_young: false, - user_plan: Some(Plan::ZedFree), - tab_index: Some(1), - } - .into_any_element(), - ), - single_example( - "Free Plan but Young Account", - AiUpsellCard { - sign_in_status: SignInStatus::SignedIn, - sign_in: Arc::new(|_, _| {}), - account_too_young: true, - user_plan: Some(Plan::ZedFree), - tab_index: Some(1), - } - .into_any_element(), - ), - single_example( - "Pro Trial", - AiUpsellCard { - sign_in_status: SignInStatus::SignedIn, - sign_in: Arc::new(|_, _| {}), - account_too_young: false, - user_plan: Some(Plan::ZedProTrial), - tab_index: Some(1), - } - .into_any_element(), - ), - single_example( - "Pro Plan", - AiUpsellCard { - sign_in_status: SignInStatus::SignedIn, - sign_in: Arc::new(|_, _| {}), - account_too_young: false, - user_plan: Some(Plan::ZedPro), - tab_index: Some(1), - } - .into_any_element(), - ), - ], - )) + .children(vec![example_group(vec![ + single_example( + "Signed Out State", + AiUpsellCard { + sign_in_status: SignInStatus::SignedOut, + sign_in: Arc::new(|_, _| {}), + } + .into_any_element(), + ), + single_example( + "Signed In State", + AiUpsellCard { + sign_in_status: SignInStatus::SignedIn, + sign_in: Arc::new(|_, _| {}), + } + .into_any_element(), + ), + ])]) .into_any_element(), ) } diff --git a/crates/ai_onboarding/src/plan_definitions.rs b/crates/ai_onboarding/src/plan_definitions.rs deleted file mode 100644 index 8d66f6c356..0000000000 --- a/crates/ai_onboarding/src/plan_definitions.rs +++ /dev/null @@ -1,39 +0,0 @@ -use gpui::{IntoElement, ParentElement}; -use ui::{List, ListBulletItem, prelude::*}; - -/// Centralized definitions for Zed AI plans -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 { - 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 { - List::new() - .child(ListBulletItem::new("150 prompts with Claude models")) - .child(ListBulletItem::new( - "Unlimited edit predictions with Zeta, our open-source model", - )) - .when(period, |this| { - this.child(ListBulletItem::new( - "Try it out for 14 days for free, no credit card required", - )) - }) - } - - pub fn pro_plan(&self, price: bool) -> impl IntoElement { - List::new() - .child(ListBulletItem::new("500 prompts with Claude models")) - .child(ListBulletItem::new( - "Unlimited edit predictions with Zeta, our open-source model", - )) - .when(price, |this| { - this.child(ListBulletItem::new("$20 USD per month")) - }) - } -} diff --git a/crates/ai_onboarding/src/young_account_banner.rs b/crates/ai_onboarding/src/young_account_banner.rs index ed9a6b3b35..a43625a60e 100644 --- a/crates/ai_onboarding/src/young_account_banner.rs +++ b/crates/ai_onboarding/src/young_account_banner.rs @@ -15,8 +15,7 @@ impl RenderOnce for YoungAccountBanner { .child(YOUNG_ACCOUNT_DISCLAIMER); div() - .max_w_full() .my_1() - .child(Banner::new().severity(Severity::Warning).child(label)) + .child(Banner::new().severity(ui::Severity::Warning).child(label)) } } diff --git a/crates/anthropic/src/anthropic.rs b/crates/anthropic/src/anthropic.rs index 3ff1666755..c73f606045 100644 --- a/crates/anthropic/src/anthropic.rs +++ b/crates/anthropic/src/anthropic.rs @@ -36,18 +36,11 @@ pub enum AnthropicModelMode { pub enum Model { #[serde(rename = "claude-opus-4", alias = "claude-opus-4-latest")] ClaudeOpus4, - #[serde(rename = "claude-opus-4-1", alias = "claude-opus-4-1-latest")] - ClaudeOpus4_1, #[serde( rename = "claude-opus-4-thinking", alias = "claude-opus-4-thinking-latest" )] ClaudeOpus4Thinking, - #[serde( - rename = "claude-opus-4-1-thinking", - alias = "claude-opus-4-1-thinking-latest" - )] - ClaudeOpus4_1Thinking, #[default] #[serde(rename = "claude-sonnet-4", alias = "claude-sonnet-4-latest")] ClaudeSonnet4, @@ -98,18 +91,10 @@ impl Model { } pub fn from_id(id: &str) -> Result { - if id.starts_with("claude-opus-4-1-thinking") { - return Ok(Self::ClaudeOpus4_1Thinking); - } - if id.starts_with("claude-opus-4-thinking") { return Ok(Self::ClaudeOpus4Thinking); } - if id.starts_with("claude-opus-4-1") { - return Ok(Self::ClaudeOpus4_1); - } - if id.starts_with("claude-opus-4") { return Ok(Self::ClaudeOpus4); } @@ -156,9 +141,7 @@ impl Model { pub fn id(&self) -> &str { match self { Self::ClaudeOpus4 => "claude-opus-4-latest", - Self::ClaudeOpus4_1 => "claude-opus-4-1-latest", Self::ClaudeOpus4Thinking => "claude-opus-4-thinking-latest", - Self::ClaudeOpus4_1Thinking => "claude-opus-4-1-thinking-latest", Self::ClaudeSonnet4 => "claude-sonnet-4-latest", Self::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking-latest", Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest", @@ -176,7 +159,6 @@ impl Model { pub fn request_id(&self) -> &str { match self { Self::ClaudeOpus4 | Self::ClaudeOpus4Thinking => "claude-opus-4-20250514", - Self::ClaudeOpus4_1 | Self::ClaudeOpus4_1Thinking => "claude-opus-4-1-20250805", Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => "claude-sonnet-4-20250514", Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest", Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => "claude-3-7-sonnet-latest", @@ -191,9 +173,7 @@ impl Model { pub fn display_name(&self) -> &str { match self { Self::ClaudeOpus4 => "Claude Opus 4", - Self::ClaudeOpus4_1 => "Claude Opus 4.1", Self::ClaudeOpus4Thinking => "Claude Opus 4 Thinking", - Self::ClaudeOpus4_1Thinking => "Claude Opus 4.1 Thinking", Self::ClaudeSonnet4 => "Claude Sonnet 4", Self::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking", Self::Claude3_7Sonnet => "Claude 3.7 Sonnet", @@ -212,9 +192,7 @@ impl Model { pub fn cache_configuration(&self) -> Option { match self { Self::ClaudeOpus4 - | Self::ClaudeOpus4_1 | Self::ClaudeOpus4Thinking - | Self::ClaudeOpus4_1Thinking | Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking | Self::Claude3_5Sonnet @@ -237,9 +215,7 @@ impl Model { pub fn max_token_count(&self) -> u64 { match self { Self::ClaudeOpus4 - | Self::ClaudeOpus4_1 | Self::ClaudeOpus4Thinking - | Self::ClaudeOpus4_1Thinking | Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking | Self::Claude3_5Sonnet @@ -256,9 +232,7 @@ impl Model { pub fn max_output_tokens(&self) -> u64 { match self { Self::ClaudeOpus4 - | Self::ClaudeOpus4_1 | Self::ClaudeOpus4Thinking - | Self::ClaudeOpus4_1Thinking | Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking | Self::Claude3_5Sonnet @@ -275,9 +249,7 @@ impl Model { pub fn default_temperature(&self) -> f32 { match self { Self::ClaudeOpus4 - | Self::ClaudeOpus4_1 | Self::ClaudeOpus4Thinking - | Self::ClaudeOpus4_1Thinking | Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking | Self::Claude3_5Sonnet @@ -297,7 +269,6 @@ impl Model { pub fn mode(&self) -> AnthropicModelMode { match self { Self::ClaudeOpus4 - | Self::ClaudeOpus4_1 | Self::ClaudeSonnet4 | Self::Claude3_5Sonnet | Self::Claude3_7Sonnet @@ -306,7 +277,6 @@ impl Model { | Self::Claude3Sonnet | Self::Claude3Haiku => AnthropicModelMode::Default, Self::ClaudeOpus4Thinking - | Self::ClaudeOpus4_1Thinking | Self::ClaudeSonnet4Thinking | Self::Claude3_7SonnetThinking => AnthropicModelMode::Thinking { budget_tokens: Some(4_096), diff --git a/crates/askpass/src/askpass.rs b/crates/askpass/src/askpass.rs index 9e84a9fed0..f085a2be72 100644 --- a/crates/askpass/src/askpass.rs +++ b/crates/askpass/src/askpass.rs @@ -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(); - AskPassResult::CancelledByUser + return AskPassResult::CancelledByUser } _ = futures::FutureExt::fuse(smol::Timer::after(connection_timeout)) => { - AskPassResult::Timedout + return AskPassResult::Timedout } } } @@ -215,7 +215,7 @@ pub fn main(socket: &str) { } #[cfg(target_os = "windows")] - while buffer.last().is_some_and(|&b| b == b'\n' || b == b'\r') { + while buffer.last().map_or(false, |&b| b == b'\n' || b == b'\r') { buffer.pop(); } if buffer.last() != Some(&b'\0') { diff --git a/crates/assets/src/assets.rs b/crates/assets/src/assets.rs index 5c7e671159..fad0c58b73 100644 --- a/crates/assets/src/assets.rs +++ b/crates/assets/src/assets.rs @@ -58,7 +58,9 @@ impl Assets { pub fn load_test_fonts(&self, cx: &App) { cx.text_system() .add_fonts(vec![ - self.load("fonts/lilex/Lilex-Regular.ttf").unwrap().unwrap(), + self.load("fonts/plex-mono/ZedPlexMono-Regular.ttf") + .unwrap() + .unwrap(), ]) .unwrap() } diff --git a/crates/assistant_context/Cargo.toml b/crates/assistant_context/Cargo.toml index 45c0072418..8f5ff98790 100644 --- a/crates/assistant_context/Cargo.toml +++ b/crates/assistant_context/Cargo.toml @@ -11,9 +11,6 @@ workspace = true [lib] path = "src/assistant_context.rs" -[features] -test-support = [] - [dependencies] agent_settings.workspace = true anyhow.workspace = true diff --git a/crates/assistant_context/src/assistant_context.rs b/crates/assistant_context/src/assistant_context.rs index 12eda0954a..4518bbff79 100644 --- a/crates/assistant_context/src/assistant_context.rs +++ b/crates/assistant_context/src/assistant_context.rs @@ -2,16 +2,16 @@ mod assistant_context_tests; mod context_store; -use agent_settings::{AgentSettings, SUMMARIZE_THREAD_PROMPT}; +use agent_settings::AgentSettings; use anyhow::{Context as _, Result, bail}; use assistant_slash_command::{ SlashCommandContent, SlashCommandEvent, SlashCommandLine, SlashCommandOutputSection, SlashCommandResult, SlashCommandWorkingSet, }; use assistant_slash_commands::FileCommandMetadata; -use client::{self, Client, ModelRequestUsage, RequestUsage, proto, telemetry::Telemetry}; +use client::{self, Client, proto, telemetry::Telemetry}; use clock::ReplicaId; -use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit}; +use cloud_llm_client::CompletionIntent; use collections::{HashMap, HashSet}; use fs::{Fs, RenameOptions}; use futures::{FutureExt, StreamExt, future::Shared}; @@ -590,16 +590,17 @@ impl From<&Message> for MessageMetadata { impl MessageMetadata { pub fn is_cache_valid(&self, buffer: &BufferSnapshot, range: &Range) -> bool { - match &self.cache { + let result = 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 } } @@ -1022,11 +1023,9 @@ impl AssistantContext { summary: new_summary, .. } => { - if self - .summary - .timestamp() - .is_none_or(|current_timestamp| new_summary.timestamp > current_timestamp) - { + if self.summary.timestamp().map_or(true, |current_timestamp| { + new_summary.timestamp > current_timestamp + }) { self.summary = ContextSummary::Content(new_summary); summary_generated = true; } @@ -1077,20 +1076,20 @@ impl AssistantContext { timestamp, .. } => { - 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; + 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; + } } + cx.emit(ContextEvent::InvokedSlashCommandChanged { command_id: id }); } - cx.emit(ContextEvent::InvokedSlashCommandChanged { command_id: id }); } } ContextOperation::BufferOperation(_) => unreachable!(), @@ -1340,7 +1339,7 @@ impl AssistantContext { let is_invalid = self .messages_metadata .get(&message_id) - .is_none_or(|metadata| { + .map_or(true, |metadata| { !metadata.is_cache_valid(&buffer, &message.offset_range) || *encountered_invalid }); @@ -1369,10 +1368,10 @@ impl AssistantContext { continue; } - if let Some(last_anchor) = last_anchor - && message.id == last_anchor - { - hit_last_anchor = true; + if let Some(last_anchor) = last_anchor { + if message.id == last_anchor { + hit_last_anchor = true; + } } new_anchor_needs_caching = new_anchor_needs_caching @@ -1407,14 +1406,14 @@ impl AssistantContext { if !self.pending_completions.is_empty() { return; } - if let Some(cache_configuration) = cache_configuration - && !cache_configuration.should_speculate - { - return; + if let Some(cache_configuration) = cache_configuration { + if !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(); @@ -1429,7 +1428,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"); @@ -1553,24 +1552,25 @@ impl AssistantContext { }) .map(ToOwned::to_owned) .collect::>(); - 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); + 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); + } } } @@ -1661,12 +1661,12 @@ impl AssistantContext { ) -> Range { 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,13 +1799,14 @@ impl AssistantContext { }); let end = this.buffer.read(cx).anchor_before(insert_position); - if run_commands_in_text - && let Some(invoked_slash_command) = + if run_commands_in_text { + if 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 => { @@ -1861,7 +1862,7 @@ impl AssistantContext { { let newline_offset = insert_position.saturating_sub(1); if buffer.contains_str_at(newline_offset, "\n") - && last_section_range.is_none_or(|last_section_range| { + && last_section_range.map_or(true, |last_section_range| { !last_section_range .to_offset(buffer) .contains(&newline_offset) @@ -2044,7 +2045,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 { @@ -2079,15 +2080,7 @@ impl AssistantContext { }); match event { - LanguageModelCompletionEvent::StatusUpdate(status_update) => { - if let CompletionRequestStatus::UsageUpdated { amount, limit } = status_update { - this.update_model_request_usage( - amount as u32, - limit, - cx, - ); - } - } + LanguageModelCompletionEvent::StatusUpdate { .. } => {} LanguageModelCompletionEvent::StartMessage { .. } => {} LanguageModelCompletionEvent::Stop(reason) => { stop_reason = reason; @@ -2282,7 +2275,7 @@ impl AssistantContext { let mut contents = self.contents(cx).peekable(); fn collect_text_content(buffer: &Buffer, range: Range) -> Option { - let text: String = buffer.text_for_range(range).collect(); + let text: String = buffer.text_for_range(range.clone()).collect(); if text.trim().is_empty() { None } else { @@ -2311,7 +2304,10 @@ impl AssistantContext { let mut request_message = LanguageModelRequestMessage { role: message.role, content: Vec::new(), - cache: message.cache.as_ref().is_some_and(|cache| cache.is_anchor), + cache: message + .cache + .as_ref() + .map_or(false, |cache| cache.is_anchor), }; while let Some(content) = contents.peek() { @@ -2681,7 +2677,10 @@ impl AssistantContext { let mut request = self.to_completion_request(Some(&model.model), cx); request.messages.push(LanguageModelRequestMessage { role: Role::User, - content: vec![SUMMARIZE_THREAD_PROMPT.into()], + content: vec![ + "Generate a concise 3-7 word title for this conversation, omitting punctuation. Go straight to the title, without any preamble and prefix like `Here's a concise suggestion:...` or `Title:`" + .into(), + ], cache: false, }); @@ -2701,7 +2700,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; @@ -2734,10 +2733,10 @@ impl AssistantContext { } this.read_with(cx, |this, _cx| { - if let Some(summary) = this.summary.content() - && summary.text.is_empty() - { - bail!("Model generated an empty summary"); + if let Some(summary) = this.summary.content() { + if summary.text.is_empty() { + bail!("Model generated an empty summary"); + } } Ok(()) })??; @@ -2792,7 +2791,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().is_some_and(|message| { + while current_message.as_ref().map_or(false, |message| { !message.offset_range.contains(&offset) && messages.peek().is_some() }) { current_message = messages.next(); @@ -2802,7 +2801,7 @@ impl AssistantContext { }; // Skip offsets that are in the same message. - while offsets.peek().is_some_and(|offset| { + while offsets.peek().map_or(false, |offset| { message.offset_range.contains(offset) || messages.peek().is_none() }) { offsets.next(); @@ -2917,18 +2916,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() - && 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() { + if 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 @@ -2957,21 +2956,6 @@ impl AssistantContext { summary.text = custom_summary; cx.emit(ContextEvent::SummaryChanged); } - - fn update_model_request_usage(&self, amount: u32, limit: UsageLimit, cx: &mut App) { - let Some(project) = &self.project else { - return; - }; - project.read(cx).user_store().update(cx, |user_store, cx| { - user_store.update_model_request_usage( - ModelRequestUsage(RequestUsage { - amount: amount as i32, - limit, - }), - cx, - ) - }); - } } #[derive(Debug, Default)] diff --git a/crates/assistant_context/src/assistant_context_tests.rs b/crates/assistant_context/src/assistant_context_tests.rs index 61d748cbdd..f139d525d3 100644 --- a/crates/assistant_context/src/assistant_context_tests.rs +++ b/crates/assistant_context/src/assistant_context_tests.rs @@ -1055,7 +1055,7 @@ fn test_mark_cache_anchors(cx: &mut App) { assert_eq!( messages_cache(&context, cx) .iter() - .filter(|(_, cache)| cache.as_ref().is_some_and(|cache| cache.is_anchor)) + .filter(|(_, cache)| cache.as_ref().map_or(false, |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().is_some_and(|cache| cache.is_anchor)) + .filter(|(_, cache)| cache.as_ref().map_or(false, |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().is_some_and(|cache| cache.is_anchor)) + .map(|(_, cache)| cache.as_ref().map_or(false, |cache| cache.is_anchor)) .collect::>(), 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().is_some_and(|cache| cache.is_anchor)) + .map(|(_, cache)| cache.as_ref().map_or(false, |cache| cache.is_anchor)) .collect::>(), vec![false, true, true, false], "Most recent message should also be cached if not a speculative request." @@ -1210,8 +1210,8 @@ async fn test_summarization(cx: &mut TestAppContext) { }); cx.run_until_parked(); - fake_model.send_last_completion_stream_text_chunk("Brief"); - fake_model.send_last_completion_stream_text_chunk(" Introduction"); + fake_model.stream_last_completion_response("Brief"); + fake_model.stream_last_completion_response(" Introduction"); fake_model.end_last_completion_stream(); cx.run_until_parked(); @@ -1274,7 +1274,7 @@ async fn test_thread_summary_error_retry(cx: &mut TestAppContext) { }); cx.run_until_parked(); - fake_model.send_last_completion_stream_text_chunk("A successful summary"); + fake_model.stream_last_completion_response("A successful summary"); fake_model.end_last_completion_stream(); cx.run_until_parked(); @@ -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, Arc) { - let registry = Arc::new(LanguageRegistry::test(cx.executor())); + let registry = Arc::new(LanguageRegistry::test(cx.executor().clone())); let fake_provider = Arc::new(FakeLanguageModelProvider::default()); let fake_model = Arc::new(fake_provider.test_model()); @@ -1356,7 +1356,7 @@ fn setup_context_editor_with_fake_model( fn simulate_successful_response(fake_model: &FakeLanguageModel, cx: &mut TestAppContext) { cx.run_until_parked(); - fake_model.send_last_completion_stream_text_chunk("Assistant response"); + fake_model.stream_last_completion_response("Assistant response"); fake_model.end_last_completion_stream(); cx.run_until_parked(); } @@ -1376,7 +1376,7 @@ fn messages_cache( context .read(cx) .messages(cx) - .map(|message| (message.id, message.cache)) + .map(|message| (message.id, message.cache.clone())) .collect() } @@ -1436,6 +1436,6 @@ impl SlashCommand for FakeSlashCommand { sections: vec![], run_commands_in_text: false, } - .into_event_stream())) + .to_event_stream())) } } diff --git a/crates/assistant_context/src/context_store.rs b/crates/assistant_context/src/context_store.rs index 6960d9db79..3090a7b234 100644 --- a/crates/assistant_context/src/context_store.rs +++ b/crates/assistant_context/src/context_store.rs @@ -138,27 +138,6 @@ impl ContextStore { }) } - #[cfg(any(test, feature = "test-support"))] - pub fn fake(project: Entity, cx: &mut Context) -> Self { - Self { - contexts: Default::default(), - contexts_metadata: Default::default(), - context_server_slash_command_ids: Default::default(), - host_contexts: Default::default(), - fs: project.read(cx).fs().clone(), - languages: project.read(cx).languages().clone(), - slash_commands: Arc::default(), - telemetry: project.read(cx).client().telemetry().clone(), - _watch_updates: Task::ready(None), - client: project.read(cx).client(), - project, - project_is_shared: false, - client_subscription: None, - _project_subscriptions: Default::default(), - prompt_builder: Arc::new(PromptBuilder::new(None).unwrap()), - } - } - async fn handle_advertise_contexts( this: Entity, envelope: TypedEnvelope, @@ -320,7 +299,7 @@ impl ContextStore { .client .subscribe_to_entity(remote_id) .log_err() - .map(|subscription| subscription.set_entity(&cx.entity(), &cx.to_async())); + .map(|subscription| subscription.set_entity(&cx.entity(), &mut cx.to_async())); self.advertise_contexts(cx); } else { self.client_subscription = None; @@ -789,7 +768,7 @@ impl ContextStore { let fs = self.fs.clone(); cx.spawn(async move |this, cx| { pub static ZED_STATELESS: LazyLock = - LazyLock::new(|| std::env::var("ZED_STATELESS").is_ok_and(|v| !v.is_empty())); + LazyLock::new(|| std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty())); if *ZED_STATELESS { return Ok(()); } @@ -862,7 +841,7 @@ impl ContextStore { ContextServerStatus::Running => { self.load_context_server_slash_commands( server_id.clone(), - context_server_store, + context_server_store.clone(), cx, ); } @@ -894,33 +873,34 @@ impl ContextStore { return; }; - if protocol.capable(context_server::protocol::ServerCapability::Prompts) - && let Some(response) = protocol + if protocol.capable(context_server::protocol::ServerCapability::Prompts) { + if let Some(response) = protocol .request::(()) .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::>(); + { + 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::>(); - this.update(cx, |this, _cx| { - this.context_server_slash_command_ids - .insert(server_id.clone(), slash_command_ids); - }) - .log_err(); + this.update(cx, |this, _cx| { + this.context_server_slash_command_ids + .insert(server_id.clone(), slash_command_ids); + }) + .log_err(); + } } }) .detach(); diff --git a/crates/assistant_slash_command/src/assistant_slash_command.rs b/crates/assistant_slash_command/src/assistant_slash_command.rs index 4b85fa2edf..828f115bf5 100644 --- a/crates/assistant_slash_command/src/assistant_slash_command.rs +++ b/crates/assistant_slash_command/src/assistant_slash_command.rs @@ -161,7 +161,7 @@ impl SlashCommandOutput { } /// Returns this [`SlashCommandOutput`] as a stream of [`SlashCommandEvent`]s. - pub fn into_event_stream(mut self) -> BoxStream<'static, Result> { + pub fn to_event_stream(mut self) -> BoxStream<'static, Result> { 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().into_event_stream().collect::>().await; + let events = output.clone().to_event_stream().collect::>().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().into_event_stream()) + SlashCommandOutput::from_event_stream(output.clone().to_event_stream()) .await .unwrap(); @@ -415,7 +415,7 @@ mod tests { run_commands_in_text: false, }; - let events = output.clone().into_event_stream().collect::>().await; + let events = output.clone().to_event_stream().collect::>().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().into_event_stream()) + SlashCommandOutput::from_event_stream(output.clone().to_event_stream()) .await .unwrap(); @@ -493,7 +493,7 @@ mod tests { run_commands_in_text: false, }; - let events = output.clone().into_event_stream().collect::>().await; + let events = output.clone().to_event_stream().collect::>().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().into_event_stream()) + SlashCommandOutput::from_event_stream(output.clone().to_event_stream()) .await .unwrap(); diff --git a/crates/assistant_slash_command/src/extension_slash_command.rs b/crates/assistant_slash_command/src/extension_slash_command.rs index e47ae52c98..74c46ffb5f 100644 --- a/crates/assistant_slash_command/src/extension_slash_command.rs +++ b/crates/assistant_slash_command/src/extension_slash_command.rs @@ -166,7 +166,7 @@ impl SlashCommand for ExtensionSlashCommand { .collect(), run_commands_in_text: false, } - .into_event_stream()) + .to_event_stream()) }) } } diff --git a/crates/assistant_slash_commands/Cargo.toml b/crates/assistant_slash_commands/Cargo.toml index c054c3ced8..f703a753f5 100644 --- a/crates/assistant_slash_commands/Cargo.toml +++ b/crates/assistant_slash_commands/Cargo.toml @@ -27,6 +27,7 @@ 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 diff --git a/crates/assistant_slash_commands/src/assistant_slash_commands.rs b/crates/assistant_slash_commands/src/assistant_slash_commands.rs index fb00a91219..fa5dd8b683 100644 --- a/crates/assistant_slash_commands/src/assistant_slash_commands.rs +++ b/crates/assistant_slash_commands/src/assistant_slash_commands.rs @@ -3,6 +3,7 @@ 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; @@ -17,6 +18,7 @@ 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::*; diff --git a/crates/assistant_slash_commands/src/cargo_workspace_command.rs b/crates/assistant_slash_commands/src/cargo_workspace_command.rs index d58b2edc4c..8b088ea012 100644 --- a/crates/assistant_slash_commands/src/cargo_workspace_command.rs +++ b/crates/assistant_slash_commands/src/cargo_workspace_command.rs @@ -150,7 +150,7 @@ impl SlashCommand for CargoWorkspaceSlashCommand { }], run_commands_in_text: false, } - .into_event_stream()) + .to_event_stream()) }) }); output.unwrap_or_else(|error| Task::ready(Err(error))) diff --git a/crates/assistant_slash_commands/src/context_server_command.rs b/crates/assistant_slash_commands/src/context_server_command.rs index ee0cbf54c2..f223d3b184 100644 --- a/crates/assistant_slash_commands/src/context_server_command.rs +++ b/crates/assistant_slash_commands/src/context_server_command.rs @@ -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 - && let Some(arg) = args.first() - { - parts.push(arg.name.as_str()); + if let Some(args) = &self.prompt.arguments { + if 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,10 +62,9 @@ impl SlashCommand for ContextServerSlashCommand { } fn requires_argument(&self) -> bool { - self.prompt - .arguments - .as_ref() - .is_some_and(|args| args.iter().any(|arg| arg.required == Some(true))) + self.prompt.arguments.as_ref().map_or(false, |args| { + args.iter().any(|arg| arg.required == Some(true)) + }) } fn complete_argument( @@ -191,7 +190,7 @@ impl SlashCommand for ContextServerSlashCommand { text: prompt, run_commands_in_text: false, } - .into_event_stream()) + .to_event_stream()) }) } else { Task::ready(Err(anyhow!("Context server not found"))) diff --git a/crates/assistant_slash_commands/src/default_command.rs b/crates/assistant_slash_commands/src/default_command.rs index 01eff881cf..6fce7f07a4 100644 --- a/crates/assistant_slash_commands/src/default_command.rs +++ b/crates/assistant_slash_commands/src/default_command.rs @@ -85,7 +85,7 @@ impl SlashCommand for DefaultSlashCommand { text, run_commands_in_text: true, } - .into_event_stream()) + .to_event_stream()) }) } } diff --git a/crates/assistant_slash_commands/src/delta_command.rs b/crates/assistant_slash_commands/src/delta_command.rs index ea05fca588..8c840c17b2 100644 --- a/crates/assistant_slash_commands/src/delta_command.rs +++ b/crates/assistant_slash_commands/src/delta_command.rs @@ -66,22 +66,23 @@ impl SlashCommand for DeltaSlashCommand { .metadata .as_ref() .and_then(|value| serde_json::from_value::(value.clone()).ok()) - && 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, - )); + 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, + )); + } } } @@ -94,31 +95,31 @@ impl SlashCommand for DeltaSlashCommand { .into_iter() .zip(file_command_new_outputs) { - 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); + 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); + } + } } } } anyhow::ensure!(changes_detected, "no new changes detected"); - Ok(output.into_event_stream()) + Ok(output.to_event_stream()) }) } } diff --git a/crates/assistant_slash_commands/src/diagnostics_command.rs b/crates/assistant_slash_commands/src/diagnostics_command.rs index 8b1dbd515c..2feabd8b1e 100644 --- a/crates/assistant_slash_commands/src/diagnostics_command.rs +++ b/crates/assistant_slash_commands/src/diagnostics_command.rs @@ -44,7 +44,7 @@ impl DiagnosticsSlashCommand { score: 0., positions: Vec::new(), worktree_id: entry.worktree_id.to_usize(), - path: entry.path, + path: entry.path.clone(), 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() - .is_some_and(|entry| entry.is_ignored), + .map_or(false, |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.into_event_stream()) + .map(|output| output.to_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 - && !path_matcher.is_match(&path) - { - continue; + if let Some(path_matcher) = &options.path_matcher { + if !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()) { diff --git a/crates/assistant_slash_commands/src/docs_command.rs b/crates/assistant_slash_commands/src/docs_command.rs new file mode 100644 index 0000000000..bd87c72849 --- /dev/null +++ b/crates/assistant_slash_commands/src/docs_command.rs @@ -0,0 +1,543 @@ +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, cx: &mut App) -> Option> { + 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>, + 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, + 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, + arguments: &[String], + _cancel: Arc, + workspace: Option>, + _: &mut Window, + cx: &mut App, + ) -> Task>> { + 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) -> Vec { + 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::>(); + 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, + arguments: &[String], + _context_slash_command_output_sections: &[SlashCommandOutputSection], + _context_buffer: BufferSnapshot, + _workspace: WeakEntity, + _delegate: Option>, + _: &mut Window, + cx: &mut App, + ) -> Task { + 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 { + match self { + Self::NoProvider => None, + Self::SearchPackageDocs { provider, .. } | Self::SearchItemDocs { provider, .. } => { + Some(provider.clone()) + } + } + } + + pub fn package(&self) -> Option { + 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() + } + ); + } +} diff --git a/crates/assistant_slash_commands/src/fetch_command.rs b/crates/assistant_slash_commands/src/fetch_command.rs index 6d3f66c9a2..5e586d4f23 100644 --- a/crates/assistant_slash_commands/src/fetch_command.rs +++ b/crates/assistant_slash_commands/src/fetch_command.rs @@ -112,7 +112,7 @@ impl SlashCommand for FetchSlashCommand { } fn icon(&self) -> IconName { - IconName::ToolWeb + IconName::Globe } fn menu_text(&self) -> String { @@ -171,13 +171,13 @@ impl SlashCommand for FetchSlashCommand { text, sections: vec![SlashCommandOutputSection { range, - icon: IconName::ToolWeb, + icon: IconName::Globe, label: format!("fetch {}", url).into(), metadata: None, }], run_commands_in_text: false, } - .into_event_stream()) + .to_event_stream()) }) } } diff --git a/crates/assistant_slash_commands/src/file_command.rs b/crates/assistant_slash_commands/src/file_command.rs index a973d653e4..c913ccc0f1 100644 --- a/crates/assistant_slash_commands/src/file_command.rs +++ b/crates/assistant_slash_commands/src/file_command.rs @@ -92,7 +92,7 @@ impl FileSlashCommand { snapshot: worktree.snapshot(), include_ignored: worktree .root_entry() - .is_some_and(|entry| entry.is_ignored), + .map_or(false, |entry| entry.is_ignored), include_root_name: true, candidates: project::Candidates::Entries, } @@ -223,7 +223,7 @@ fn collect_files( cx: &mut App, ) -> impl Stream> + use<> { let Ok(matchers) = glob_inputs - .iter() + .into_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.into_event_stream(); + let mut buffer_events = output.to_event_stream(); while let Some(event) = buffer_events.next().await { events_tx.unbounded_send(event)?; } @@ -379,7 +379,7 @@ fn collect_files( } } - while directory_stack.pop().is_some() { + while let Some(_) = directory_stack.pop() { events_tx.unbounded_send(Ok(SlashCommandEvent::EndSection))?; } } @@ -491,7 +491,7 @@ mod custom_path_matcher { impl PathMatcher { pub fn new(globs: &[String]) -> Result { let globs = globs - .iter() + .into_iter() .map(|glob| Glob::new(&SanitizedPath::from(glob).to_glob_string())) .collect::, _>>()?; let sources = globs.iter().map(|glob| glob.glob().to_owned()).collect(); @@ -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) { - false + return false; } else { self.glob.is_match(path_str.to_string() + separator) } diff --git a/crates/assistant_slash_commands/src/now_command.rs b/crates/assistant_slash_commands/src/now_command.rs index aec21e7173..e4abef2a7c 100644 --- a/crates/assistant_slash_commands/src/now_command.rs +++ b/crates/assistant_slash_commands/src/now_command.rs @@ -66,6 +66,6 @@ impl SlashCommand for NowSlashCommand { }], run_commands_in_text: false, } - .into_event_stream())) + .to_event_stream())) } } diff --git a/crates/assistant_slash_commands/src/prompt_command.rs b/crates/assistant_slash_commands/src/prompt_command.rs index bbd6d3e3ad..c177f9f359 100644 --- a/crates/assistant_slash_commands/src/prompt_command.rs +++ b/crates/assistant_slash_commands/src/prompt_command.rs @@ -80,7 +80,7 @@ impl SlashCommand for PromptSlashCommand { }; let store = PromptStore::global(cx); - let title = SharedString::from(title); + let title = SharedString::from(title.clone()); let prompt = cx.spawn({ let title = title.clone(); async move |cx| { @@ -117,7 +117,7 @@ impl SlashCommand for PromptSlashCommand { }], run_commands_in_text: true, } - .into_event_stream()) + .to_event_stream()) }) } } diff --git a/crates/assistant_slash_commands/src/symbols_command.rs b/crates/assistant_slash_commands/src/symbols_command.rs index 3028709144..ef93146431 100644 --- a/crates/assistant_slash_commands/src/symbols_command.rs +++ b/crates/assistant_slash_commands/src/symbols_command.rs @@ -92,7 +92,7 @@ impl SlashCommand for OutlineSlashCommand { text: outline_text, run_commands_in_text: false, } - .into_event_stream()) + .to_event_stream()) }) }); diff --git a/crates/assistant_slash_commands/src/tab_command.rs b/crates/assistant_slash_commands/src/tab_command.rs index a124beed63..ca7601bc4c 100644 --- a/crates/assistant_slash_commands/src/tab_command.rs +++ b/crates/assistant_slash_commands/src/tab_command.rs @@ -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.into_event_stream()) + Ok(output.to_event_stream()) }) } } @@ -195,14 +195,16 @@ fn tab_items_for_queries( } for editor in workspace.items_of_type::(cx) { - if let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() - && let Some(timestamp) = + if let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() { + if let Some(timestamp) = timestamps_by_entity_id.get(&editor.entity_id()) - && 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)); + { + 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)); + } + } } } diff --git a/crates/assistant_tool/Cargo.toml b/crates/assistant_tool/Cargo.toml index c95695052a..acbe674b02 100644 --- a/crates/assistant_tool/Cargo.toml +++ b/crates/assistant_tool/Cargo.toml @@ -12,10 +12,12 @@ workspace = true path = "src/assistant_tool.rs" [dependencies] -action_log.workspace = true anyhow.workspace = true +buffer_diff.workspace = true +clock.workspace = true collections.workspace = true derive_more.workspace = true +futures.workspace = true gpui.workspace = true icons.workspace = true language.workspace = true @@ -28,6 +30,7 @@ serde.workspace = true serde_json.workspace = true text.workspace = true util.workspace = true +watch.workspace = true workspace.workspace = true workspace-hack.workspace = true diff --git a/crates/action_log/src/action_log.rs b/crates/assistant_tool/src/action_log.rs similarity index 93% rename from crates/action_log/src/action_log.rs rename to crates/assistant_tool/src/action_log.rs index 9ec10f4dbb..672c048872 100644 --- a/crates/action_log/src/action_log.rs +++ b/crates/assistant_tool/src/action_log.rs @@ -17,6 +17,8 @@ use util::{ pub struct ActionLog { /// Buffers that we want to notify the model about when they change. tracked_buffers: BTreeMap, TrackedBuffer>, + /// Has the model edited a file since it last checked diagnostics? + edited_since_project_diagnostics_check: bool, /// The project this action log is associated with project: Entity, } @@ -26,6 +28,7 @@ impl ActionLog { pub fn new(project: Entity) -> Self { Self { tracked_buffers: BTreeMap::default(), + edited_since_project_diagnostics_check: false, project, } } @@ -34,6 +37,16 @@ impl ActionLog { &self.project } + /// Notifies a diagnostics check + pub fn checked_project_diagnostics(&mut self) { + self.edited_since_project_diagnostics_check = false; + } + + /// Returns true if any files have been edited since the last project diagnostics check + pub fn has_edited_files_since_project_diagnostics_check(&self) -> bool { + self.edited_since_project_diagnostics_check + } + pub fn latest_snapshot(&self, buffer: &Entity) -> Option { Some(self.tracked_buffers.get(buffer)?.snapshot.clone()) } @@ -116,7 +129,7 @@ impl ActionLog { } else if buffer .read(cx) .file() - .is_some_and(|file| file.disk_state().exists()) + .map_or(false, |file| file.disk_state().exists()) { TrackedBufferStatus::Created { existing_file_content: Some(buffer.read(cx).as_rope().clone()), @@ -161,7 +174,7 @@ impl ActionLog { diff_base, last_seen_base, unreviewed_edits, - snapshot: text_snapshot, + snapshot: text_snapshot.clone(), status, version: buffer.read(cx).version(), diff, @@ -190,7 +203,7 @@ impl ActionLog { cx: &mut Context, ) { 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 +228,7 @@ impl ActionLog { if buffer .read(cx) .file() - .is_some_and(|file| file.disk_state() == DiskState::Deleted) + .map_or(false, |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 +240,7 @@ impl ActionLog { if buffer .read(cx) .file() - .is_some_and(|file| file.disk_state() != DiskState::Deleted) + .map_or(false, |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,14 +277,15 @@ 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| { - if let buffer_diff::BufferDiffEvent::DiffChanged { .. } = event { + Some(cx.subscribe(git_diff, move |_, event, cx| match event { + buffer_diff::BufferDiffEvent::DiffChanged { .. } => { 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 { @@ -289,7 +303,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?; } } } @@ -461,7 +475,7 @@ impl ActionLog { anyhow::Ok(( tracked_buffer.diff.clone(), buffer.read(cx).language().cloned(), - buffer.read(cx).language_registry(), + buffer.read(cx).language_registry().clone(), )) })??; let diff_snapshot = BufferDiff::update_diff( @@ -497,7 +511,7 @@ impl ActionLog { new: new_range, }, &new_diff_base, - buffer_snapshot.as_rope(), + &buffer_snapshot.as_rope(), )); } unreviewed_edits @@ -529,12 +543,15 @@ 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, cx: &mut Context) { - self.track_buffer_internal(buffer, true, cx); + self.edited_since_project_diagnostics_check = true; + self.track_buffer_internal(buffer.clone(), 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, cx: &mut Context) { - let tracked_buffer = self.track_buffer_internal(buffer, false, cx); + self.edited_since_project_diagnostics_check = true; + + let tracked_buffer = self.track_buffer_internal(buffer.clone(), false, cx); if let TrackedBufferStatus::Deleted = tracked_buffer.status { tracked_buffer.status = TrackedBufferStatus::Modified; } @@ -613,11 +630,6 @@ impl ActionLog { false } }); - 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); } } @@ -763,9 +775,6 @@ impl ActionLog { .retain(|_buffer, tracked_buffer| match tracked_buffer.status { TrackedBufferStatus::Deleted => false, _ => { - if let TrackedBufferStatus::Created { .. } = &mut tracked_buffer.status { - tracked_buffer.status = TrackedBufferStatus::Modified; - } tracked_buffer.unreviewed_edits.clear(); tracked_buffer.diff_base = tracked_buffer.snapshot.as_rope().clone(); tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx); @@ -810,7 +819,7 @@ impl ActionLog { tracked.version != buffer.version && buffer .file() - .is_some_and(|file| file.disk_state() != DiskState::Deleted) + .map_or(false, |file| file.disk_state() != DiskState::Deleted) }) .map(|(buffer, _)| buffer) } @@ -846,7 +855,7 @@ fn apply_non_conflicting_edits( conflict = true; if new_edits .peek() - .is_some_and(|next_edit| next_edit.old.overlaps(&old_edit.new)) + .map_or(false, |next_edit| next_edit.old.overlaps(&old_edit.new)) { new_edit = new_edits.next().unwrap(); } else { @@ -963,7 +972,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() } @@ -2066,134 +2075,6 @@ mod tests { assert_eq!(content, "ai content\nuser added this line"); } - #[gpui::test] - async fn test_reject_after_accepting_hunk_on_created_file(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - - let file_path = project - .read_with(cx, |project, cx| { - project.find_project_path("dir/new_file", cx) - }) - .unwrap(); - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path.clone(), cx)) - .await - .unwrap(); - - // AI creates file with initial content - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| buffer.set_text("ai content v1", cx)); - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - }); - project - .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) - .await - .unwrap(); - cx.run_until_parked(); - assert_ne!(unreviewed_hunks(&action_log, cx), vec![]); - - // User accepts the single hunk - action_log.update(cx, |log, cx| { - log.keep_edits_in_range(buffer.clone(), Anchor::MIN..Anchor::MAX, cx) - }); - cx.run_until_parked(); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - assert!(fs.is_file(path!("/dir/new_file").as_ref()).await); - - // AI modifies the file - cx.update(|cx| { - buffer.update(cx, |buffer, cx| buffer.set_text("ai content v2", cx)); - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - }); - project - .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) - .await - .unwrap(); - cx.run_until_parked(); - assert_ne!(unreviewed_hunks(&action_log, cx), vec![]); - - // User rejects the hunk - action_log - .update(cx, |log, cx| { - log.reject_edits_in_ranges(buffer.clone(), vec![Anchor::MIN..Anchor::MAX], cx) - }) - .await - .unwrap(); - cx.run_until_parked(); - assert!(fs.is_file(path!("/dir/new_file").as_ref()).await,); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "ai content v1" - ); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - - #[gpui::test] - async fn test_reject_edits_on_previously_accepted_created_file(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - - let file_path = project - .read_with(cx, |project, cx| { - project.find_project_path("dir/new_file", cx) - }) - .unwrap(); - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path.clone(), cx)) - .await - .unwrap(); - - // AI creates file with initial content - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| buffer.set_text("ai content v1", cx)); - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - }); - project - .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) - .await - .unwrap(); - cx.run_until_parked(); - - // User clicks "Accept All" - action_log.update(cx, |log, cx| log.keep_all_edits(cx)); - cx.run_until_parked(); - assert!(fs.is_file(path!("/dir/new_file").as_ref()).await); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); // Hunks are cleared - - // AI modifies file again - cx.update(|cx| { - buffer.update(cx, |buffer, cx| buffer.set_text("ai content v2", cx)); - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - }); - project - .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) - .await - .unwrap(); - cx.run_until_parked(); - assert_ne!(unreviewed_hunks(&action_log, cx), vec![]); - - // User clicks "Reject All" - action_log - .update(cx, |log, cx| log.reject_all_edits(cx)) - .await; - cx.run_until_parked(); - assert!(fs.is_file(path!("/dir/new_file").as_ref()).await); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "ai content v1" - ); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - #[gpui::test(iterations = 100)] async fn test_random_diffs(mut rng: StdRng, cx: &mut TestAppContext) { init_test(cx); @@ -2267,7 +2148,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() { @@ -2425,7 +2306,7 @@ mod tests { assert_eq!( unreviewed_hunks(&action_log, cx), vec![( - buffer, + buffer.clone(), vec![ HunkStatus { range: Point::new(6, 0)..Point::new(7, 0), diff --git a/crates/assistant_tool/src/assistant_tool.rs b/crates/assistant_tool/src/assistant_tool.rs index 9c5825d0f0..22cbaac3f8 100644 --- a/crates/assistant_tool/src/assistant_tool.rs +++ b/crates/assistant_tool/src/assistant_tool.rs @@ -1,3 +1,4 @@ +mod action_log; pub mod outline; mod tool_registry; mod tool_schema; @@ -9,7 +10,6 @@ use std::fmt::Formatter; use std::ops::Deref; use std::sync::Arc; -use action_log::ActionLog; use anyhow::Result; use gpui::AnyElement; use gpui::AnyWindowHandle; @@ -25,6 +25,7 @@ use language_model::LanguageModelToolSchemaFormat; use project::Project; use workspace::Workspace; +pub use crate::action_log::*; pub use crate::tool_registry::*; pub use crate::tool_schema::*; pub use crate::tool_working_set::*; diff --git a/crates/assistant_tool/src/outline.rs b/crates/assistant_tool/src/outline.rs index 4f8bde5456..6af204d79a 100644 --- a/crates/assistant_tool/src/outline.rs +++ b/crates/assistant_tool/src/outline.rs @@ -1,4 +1,4 @@ -use action_log::ActionLog; +use crate::ActionLog; use anyhow::{Context as _, Result}; use gpui::{AsyncApp, Entity}; use language::{OutlineItem, ParseStatus}; diff --git a/crates/assistant_tool/src/tool_schema.rs b/crates/assistant_tool/src/tool_schema.rs index 192f7c8a2b..7b48f93ba6 100644 --- a/crates/assistant_tool/src/tool_schema.rs +++ b/crates/assistant_tool/src/tool_schema.rs @@ -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 - && 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 { + 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)); + } - // 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) - && predicate(value) - { - obj.remove(key); + if let Some(value) = obj.get(key) { + if 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") - && 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") { + if 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 diff --git a/crates/assistant_tool/src/tool_working_set.rs b/crates/assistant_tool/src/tool_working_set.rs index 61f57affc7..c0a358917b 100644 --- a/crates/assistant_tool/src/tool_working_set.rs +++ b/crates/assistant_tool/src/tool_working_set.rs @@ -156,13 +156,13 @@ fn resolve_context_server_tool_name_conflicts( if duplicated_tool_names.is_empty() { return context_server_tools - .iter() + .into_iter() .map(|tool| (resolve_tool_name(tool).into(), tool.clone())) .collect(); } context_server_tools - .iter() + .into_iter() .filter_map(|tool| { let mut tool_name = resolve_tool_name(tool); if !duplicated_tool_names.contains(&tool_name) { diff --git a/crates/assistant_tools/Cargo.toml b/crates/assistant_tools/Cargo.toml index 5a8ca8a5e9..d4b8fa3afc 100644 --- a/crates/assistant_tools/Cargo.toml +++ b/crates/assistant_tools/Cargo.toml @@ -15,7 +15,6 @@ path = "src/assistant_tools.rs" eval = [] [dependencies] -action_log.workspace = true agent_settings.workspace = true anyhow.workspace = true assistant_tool.workspace = true diff --git a/crates/assistant_tools/src/assistant_tools.rs b/crates/assistant_tools/src/assistant_tools.rs index ce3b639cb2..57fdc51336 100644 --- a/crates/assistant_tools/src/assistant_tools.rs +++ b/crates/assistant_tools/src/assistant_tools.rs @@ -2,7 +2,7 @@ mod copy_path_tool; mod create_directory_tool; mod delete_path_tool; mod diagnostics_tool; -pub mod edit_agent; +mod edit_agent; mod edit_file_tool; mod fetch_tool; mod find_path_tool; @@ -14,7 +14,7 @@ mod open_tool; mod project_notifications_tool; mod read_file_tool; mod schema; -pub mod templates; +mod templates; mod terminal_tool; mod thinking_tool; mod ui; @@ -36,12 +36,13 @@ use crate::delete_path_tool::DeletePathTool; use crate::diagnostics_tool::DiagnosticsTool; use crate::edit_file_tool::EditFileTool; use crate::fetch_tool::FetchTool; +use crate::find_path_tool::FindPathTool; use crate::list_directory_tool::ListDirectoryTool; use crate::now_tool::NowTool; use crate::thinking_tool::ThinkingTool; pub use edit_file_tool::{EditFileMode, EditFileToolInput}; -pub use find_path_tool::*; +pub use find_path_tool::FindPathToolInput; pub use grep_tool::{GrepTool, GrepToolInput}; pub use open_tool::OpenTool; pub use project_notifications_tool::ProjectNotificationsTool; @@ -72,10 +73,11 @@ pub fn init(http_client: Arc, cx: &mut App) { register_web_search_tool(&LanguageModelRegistry::global(cx), cx); cx.subscribe( &LanguageModelRegistry::global(cx), - move |registry, event, cx| { - if let language_model::Event::DefaultModelChanged = event { + move |registry, event, cx| match event { + language_model::Event::DefaultModelChanged => { register_web_search_tool(®istry, cx); } + _ => {} }, ) .detach(); @@ -85,7 +87,7 @@ fn register_web_search_tool(registry: &Entity, cx: &mut A let using_zed_provider = registry .read(cx) .default_model() - .is_some_and(|default| default.is_provided_by_zed()); + .map_or(false, |default| default.is_provided_by_zed()); if using_zed_provider { ToolRegistry::global(cx).register_tool(WebSearchTool); } else { diff --git a/crates/assistant_tools/src/copy_path_tool.rs b/crates/assistant_tools/src/copy_path_tool.rs index c56a864bd4..e34ae9ff93 100644 --- a/crates/assistant_tools/src/copy_path_tool.rs +++ b/crates/assistant_tools/src/copy_path_tool.rs @@ -1,7 +1,6 @@ use crate::schema::json_schema_for; -use action_log::ActionLog; use anyhow::{Context as _, Result, anyhow}; -use assistant_tool::{Tool, ToolResult}; +use assistant_tool::{ActionLog, Tool, ToolResult}; use gpui::AnyWindowHandle; use gpui::{App, AppContext, Entity, Task}; use language_model::LanguageModel; diff --git a/crates/assistant_tools/src/create_directory_tool.rs b/crates/assistant_tools/src/create_directory_tool.rs index 85eea463dc..11d969d234 100644 --- a/crates/assistant_tools/src/create_directory_tool.rs +++ b/crates/assistant_tools/src/create_directory_tool.rs @@ -1,7 +1,6 @@ use crate::schema::json_schema_for; -use action_log::ActionLog; use anyhow::{Context as _, Result, anyhow}; -use assistant_tool::{Tool, ToolResult}; +use assistant_tool::{ActionLog, Tool, ToolResult}; use gpui::AnyWindowHandle; use gpui::{App, Entity, Task}; use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; diff --git a/crates/assistant_tools/src/delete_path_tool.rs b/crates/assistant_tools/src/delete_path_tool.rs index b181eeff5c..9e69c18b65 100644 --- a/crates/assistant_tools/src/delete_path_tool.rs +++ b/crates/assistant_tools/src/delete_path_tool.rs @@ -1,7 +1,6 @@ use crate::schema::json_schema_for; -use action_log::ActionLog; use anyhow::{Context as _, Result, anyhow}; -use assistant_tool::{Tool, ToolResult}; +use assistant_tool::{ActionLog, Tool, ToolResult}; use futures::{SinkExt, StreamExt, channel::mpsc}; use gpui::{AnyWindowHandle, App, AppContext, Entity, Task}; use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; diff --git a/crates/assistant_tools/src/diagnostics_tool.rs b/crates/assistant_tools/src/diagnostics_tool.rs index 4ec794e127..12ab97f820 100644 --- a/crates/assistant_tools/src/diagnostics_tool.rs +++ b/crates/assistant_tools/src/diagnostics_tool.rs @@ -1,7 +1,6 @@ use crate::schema::json_schema_for; -use action_log::ActionLog; use anyhow::{Result, anyhow}; -use assistant_tool::{Tool, ToolResult}; +use assistant_tool::{ActionLog, Tool, ToolResult}; use gpui::{AnyWindowHandle, App, Entity, Task}; use language::{DiagnosticSeverity, OffsetRangeExt}; use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; @@ -86,7 +85,7 @@ impl Tool for DiagnosticsTool { input: serde_json::Value, _request: Arc, project: Entity, - _action_log: Entity, + action_log: Entity, _model: Arc, _window: Option, cx: &mut App, @@ -159,6 +158,10 @@ impl Tool for DiagnosticsTool { } } + action_log.update(cx, |action_log, _cx| { + action_log.checked_project_diagnostics(); + }); + if has_diagnostics { Task::ready(Ok(output.into())).into() } else { diff --git a/crates/assistant_tools/src/edit_agent.rs b/crates/assistant_tools/src/edit_agent.rs index 665ece2baa..fed79434bb 100644 --- a/crates/assistant_tools/src/edit_agent.rs +++ b/crates/assistant_tools/src/edit_agent.rs @@ -5,8 +5,8 @@ mod evals; mod streaming_fuzzy_matcher; use crate::{Template, Templates}; -use action_log::ActionLog; use anyhow::Result; +use assistant_tool::ActionLog; use cloud_llm_client::CompletionIntent; use create_file_parser::{CreateFileParser, CreateFileParserEvent}; pub use edit_parser::EditFormat; @@ -29,6 +29,7 @@ use serde::{Deserialize, Serialize}; use std::{cmp, iter, mem, ops::Range, path::PathBuf, pin::Pin, sync::Arc, task::Poll}; use streaming_diff::{CharOperation, StreamingDiff}; use streaming_fuzzy_matcher::StreamingFuzzyMatcher; +use util::debug_panic; #[derive(Serialize)] struct CreateFilePromptTemplate { @@ -65,7 +66,7 @@ pub enum EditAgentOutputEvent { ResolvingEditRange(Range), UnresolvedEditRange, AmbiguousEditRange(Vec>), - Edited(Range), + Edited, } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] @@ -178,9 +179,7 @@ impl EditAgent { ) }); output_events_tx - .unbounded_send(EditAgentOutputEvent::Edited( - language::Anchor::MIN..language::Anchor::MAX, - )) + .unbounded_send(EditAgentOutputEvent::Edited) .ok(); })?; @@ -202,9 +201,7 @@ impl EditAgent { }); })?; output_events_tx - .unbounded_send(EditAgentOutputEvent::Edited( - language::Anchor::MIN..language::Anchor::MAX, - )) + .unbounded_send(EditAgentOutputEvent::Edited) .ok(); } } @@ -340,8 +337,8 @@ impl EditAgent { // Edit the buffer and report edits to the action log as part of the // same effect cycle, otherwise the edit will be reported as if the // user made it. - let (min_edit_start, max_edit_end) = cx.update(|cx| { - let (min_edit_start, max_edit_end) = buffer.update(cx, |buffer, cx| { + cx.update(|cx| { + let max_edit_end = buffer.update(cx, |buffer, cx| { buffer.edit(edits.iter().cloned(), None, cx); let max_edit_end = buffer .summaries_for_anchors::( @@ -349,16 +346,7 @@ impl EditAgent { ) .max() .unwrap(); - let min_edit_start = buffer - .summaries_for_anchors::( - edits.iter().map(|(range, _)| &range.start), - ) - .min() - .unwrap(); - ( - buffer.anchor_after(min_edit_start), - buffer.anchor_before(max_edit_end), - ) + buffer.anchor_before(max_edit_end) }); self.action_log .update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); @@ -371,10 +359,9 @@ impl EditAgent { cx, ); }); - (min_edit_start, max_edit_end) })?; output_events - .unbounded_send(EditAgentOutputEvent::Edited(min_edit_start..max_edit_end)) + .unbounded_send(EditAgentOutputEvent::Edited) .ok(); } @@ -672,30 +659,34 @@ impl EditAgent { cx: &mut AsyncApp, ) -> Result>> { let mut messages_iter = conversation.messages.iter_mut(); - 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(); + 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 - && let Some(prev_message) = messages_iter.next_back() - { - last_message.cache = false; - prev_message.cache = true; - } + // 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 last_message.content.is_empty() { - conversation.messages.pop(); + if last_message.content.is_empty() { + conversation.messages.pop(); + } + } else { + debug_panic!( + "Last message must be an Assistant tool calling! Got {:?}", + last_message.content + ); } } @@ -770,7 +761,6 @@ mod tests { use gpui::{AppContext, TestAppContext}; use indoc::indoc; use language_model::fake_provider::FakeLanguageModel; - use pretty_assertions::assert_matches; use project::{AgentLocation, Project}; use rand::prelude::*; use rand::rngs::StdRng; @@ -972,7 +962,7 @@ mod tests { ); cx.run_until_parked(); - model.send_last_completion_stream_text_chunk("a"); + model.stream_last_completion_response("a"); cx.run_until_parked(); assert_eq!(drain_events(&mut events), vec![]); assert_eq!( @@ -984,7 +974,7 @@ mod tests { None ); - model.send_last_completion_stream_text_chunk("bc"); + model.stream_last_completion_response("bc"); cx.run_until_parked(); assert_eq!( drain_events(&mut events), @@ -1006,12 +996,9 @@ mod tests { }) ); - model.send_last_completion_stream_text_chunk("abX"); + model.stream_last_completion_response("abX"); cx.run_until_parked(); - assert_matches!( - drain_events(&mut events).as_slice(), - [EditAgentOutputEvent::Edited(_)] - ); + assert_eq!(drain_events(&mut events), [EditAgentOutputEvent::Edited]); assert_eq!( buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), "abXc\ndef\nghi\njkl" @@ -1024,12 +1011,9 @@ mod tests { }) ); - model.send_last_completion_stream_text_chunk("cY"); + model.stream_last_completion_response("cY"); cx.run_until_parked(); - assert_matches!( - drain_events(&mut events).as_slice(), - [EditAgentOutputEvent::Edited { .. }] - ); + assert_eq!(drain_events(&mut events), [EditAgentOutputEvent::Edited]); assert_eq!( buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), "abXcY\ndef\nghi\njkl" @@ -1042,8 +1026,8 @@ mod tests { }) ); - model.send_last_completion_stream_text_chunk(""); - model.send_last_completion_stream_text_chunk("hall"); + model.stream_last_completion_response(""); + model.stream_last_completion_response("hall"); cx.run_until_parked(); assert_eq!(drain_events(&mut events), vec![]); assert_eq!( @@ -1058,8 +1042,8 @@ mod tests { }) ); - model.send_last_completion_stream_text_chunk("ucinated old"); - model.send_last_completion_stream_text_chunk(""); + model.stream_last_completion_response("ucinated old"); + model.stream_last_completion_response(""); cx.run_until_parked(); assert_eq!( drain_events(&mut events), @@ -1077,8 +1061,8 @@ mod tests { }) ); - model.send_last_completion_stream_text_chunk("hallucinated new"); + model.stream_last_completion_response("hallucinated new"); cx.run_until_parked(); assert_eq!(drain_events(&mut events), vec![]); assert_eq!( @@ -1093,7 +1077,7 @@ mod tests { }) ); - model.send_last_completion_stream_text_chunk("\nghi\nj"); + model.stream_last_completion_response("\nghi\nj"); cx.run_until_parked(); assert_eq!( drain_events(&mut events), @@ -1115,8 +1099,8 @@ mod tests { }) ); - model.send_last_completion_stream_text_chunk("kl"); - model.send_last_completion_stream_text_chunk(""); + model.stream_last_completion_response("kl"); + model.stream_last_completion_response(""); cx.run_until_parked(); assert_eq!( drain_events(&mut events), @@ -1138,11 +1122,11 @@ mod tests { }) ); - model.send_last_completion_stream_text_chunk("GHI"); + model.stream_last_completion_response("GHI"); cx.run_until_parked(); - assert_matches!( - drain_events(&mut events).as_slice(), - [EditAgentOutputEvent::Edited { .. }] + assert_eq!( + drain_events(&mut events), + vec![EditAgentOutputEvent::Edited] ); assert_eq!( buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), @@ -1187,9 +1171,9 @@ mod tests { ); cx.run_until_parked(); - assert_matches!( - drain_events(&mut events).as_slice(), - [EditAgentOutputEvent::Edited(_)] + assert_eq!( + drain_events(&mut events), + vec![EditAgentOutputEvent::Edited] ); assert_eq!( buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), @@ -1205,9 +1189,9 @@ mod tests { chunks_tx.unbounded_send("```\njkl\n").unwrap(); cx.run_until_parked(); - assert_matches!( - drain_events(&mut events).as_slice(), - [EditAgentOutputEvent::Edited { .. }] + assert_eq!( + drain_events(&mut events), + vec![EditAgentOutputEvent::Edited] ); assert_eq!( buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), @@ -1223,9 +1207,9 @@ mod tests { chunks_tx.unbounded_send("mno\n").unwrap(); cx.run_until_parked(); - assert_matches!( - drain_events(&mut events).as_slice(), - [EditAgentOutputEvent::Edited { .. }] + assert_eq!( + drain_events(&mut events), + vec![EditAgentOutputEvent::Edited] ); assert_eq!( buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), @@ -1241,9 +1225,9 @@ mod tests { chunks_tx.unbounded_send("pqr\n```").unwrap(); cx.run_until_parked(); - assert_matches!( - drain_events(&mut events).as_slice(), - [EditAgentOutputEvent::Edited(_)], + assert_eq!( + drain_events(&mut events), + vec![EditAgentOutputEvent::Edited] ); assert_eq!( buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), @@ -1383,9 +1367,7 @@ mod tests { cx.background_spawn(async move { for chunk in chunks { executor.simulate_random_delay().await; - model - .as_fake() - .send_last_completion_stream_text_chunk(chunk); + model.as_fake().stream_last_completion_response(chunk); } model.as_fake().end_last_completion_stream(); }) diff --git a/crates/assistant_tools/src/edit_agent/create_file_parser.rs b/crates/assistant_tools/src/edit_agent/create_file_parser.rs index 0aad9ecb87..07c8fac7b9 100644 --- a/crates/assistant_tools/src/edit_agent/create_file_parser.rs +++ b/crates/assistant_tools/src/edit_agent/create_file_parser.rs @@ -1,11 +1,10 @@ -use std::sync::OnceLock; - use regex::Regex; use smallvec::SmallVec; +use std::cell::LazyCell; use util::debug_panic; -static START_MARKER: OnceLock = OnceLock::new(); -static END_MARKER: OnceLock = OnceLock::new(); +const START_MARKER: LazyCell = LazyCell::new(|| Regex::new(r"\n?```\S*\n").unwrap()); +const END_MARKER: LazyCell = LazyCell::new(|| Regex::new(r"(^|\n)```\s*$").unwrap()); #[derive(Debug)] pub enum CreateFileParserEvent { @@ -44,12 +43,10 @@ impl CreateFileParser { self.buffer.push_str(chunk); let mut edit_events = SmallVec::new(); - let start_marker_regex = START_MARKER.get_or_init(|| Regex::new(r"\n?```\S*\n").unwrap()); - let end_marker_regex = END_MARKER.get_or_init(|| Regex::new(r"(^|\n)```\s*$").unwrap()); loop { match &mut self.state { ParserState::Pending => { - if let Some(m) = start_marker_regex.find(&self.buffer) { + if let Some(m) = START_MARKER.find(&self.buffer) { self.buffer.drain(..m.end()); self.state = ParserState::WithinText; } else { @@ -68,7 +65,7 @@ impl CreateFileParser { break; } ParserState::Finishing => { - if let Some(m) = end_marker_regex.find(&self.buffer) { + if let Some(m) = END_MARKER.find(&self.buffer) { self.buffer.drain(m.start()..); } if !self.buffer.is_empty() { diff --git a/crates/assistant_tools/src/edit_agent/evals.rs b/crates/assistant_tools/src/edit_agent/evals.rs index 4f182b3148..13619da25c 100644 --- a/crates/assistant_tools/src/edit_agent/evals.rs +++ b/crates/assistant_tools/src/edit_agent/evals.rs @@ -7,7 +7,7 @@ use crate::{ }; use Role::*; use assistant_tool::ToolRegistry; -use client::{Client, UserStore}; +use client::{Client, CloudUserStore, UserStore}; use collections::HashMap; use fs::FakeFs; use futures::{FutureExt, future::LocalBoxFuture}; @@ -1153,7 +1153,8 @@ 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).unwrap(); + let edit_file_input: EditFileToolInput = + serde_json::from_value(tool_use.input.clone()).unwrap(); EvalInput { conversation, @@ -1282,14 +1283,14 @@ impl EvalAssertion { // Parse the score from the response let re = regex::Regex::new(r"(\d+)").unwrap(); - 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), - }); + 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), + }); + } } anyhow::bail!("No score found in response. Raw output: {output}"); @@ -1459,7 +1460,7 @@ impl EditAgentTest { async fn new(cx: &mut TestAppContext) -> Self { cx.executor().allow_parking(); - let fs = FakeFs::new(cx.executor()); + let fs = FakeFs::new(cx.executor().clone()); cx.update(|cx| { settings::init(cx); gpui_tokio::init(cx); @@ -1469,12 +1470,14 @@ impl EditAgentTest { client::init_settings(cx); let client = Client::production(cx); let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); + let cloud_user_store = + cx.new(|cx| CloudUserStore::new(client.cloud_client(), user_store.clone(), cx)); settings::init(cx); Project::init_settings(cx); language::init(cx); language_model::init(client.clone(), cx); - language_models::init(user_store, client.clone(), cx); + language_models::init(user_store.clone(), cloud_user_store, client.clone(), cx); crate::init(client.http_client(), cx); }); @@ -1585,7 +1588,7 @@ impl EditAgentTest { let has_system_prompt = eval .conversation .first() - .is_some_and(|msg| msg.role == Role::System); + .map_or(false, |msg| msg.role == Role::System); let messages = if has_system_prompt { eval.conversation } else { @@ -1657,24 +1660,23 @@ impl EditAgentTest { } async fn retry_on_rate_limit(mut request: impl AsyncFnMut() -> Result) -> Result { - const MAX_RETRIES: usize = 20; let mut attempt = 0; - loop { attempt += 1; - let response = request().await; - - if attempt >= MAX_RETRIES { - return response; - } - - let retry_delay = match &response { - Ok(_) => None, - Err(err) => match err.downcast_ref::() { - Some(err) => match &err { + match request().await { + Ok(result) => return Ok(result), + Err(err) => match err.downcast::() { + Ok(err) => match &err { LanguageModelCompletionError::RateLimitExceeded { retry_after, .. } | LanguageModelCompletionError::ServerOverloaded { retry_after, .. } => { - Some(retry_after.unwrap_or(Duration::from_secs(5))) + let retry_after = retry_after.unwrap_or(Duration::from_secs(5)); + // Wait for the duration supplied, with some jitter to avoid all requests being made at the same time. + let jitter = retry_after.mul_f64(rand::thread_rng().gen_range(0.0..1.0)); + eprintln!( + "Attempt #{attempt}: {err}. Retry after {retry_after:?} + jitter of {jitter:?}" + ); + Timer::after(retry_after + jitter).await; + continue; } LanguageModelCompletionError::UpstreamProviderError { status, @@ -1687,31 +1689,23 @@ async fn retry_on_rate_limit(mut request: impl AsyncFnMut() -> Result) -> StatusCode::TOO_MANY_REQUESTS | StatusCode::SERVICE_UNAVAILABLE ) || status.as_u16() == 529; - if should_retry { - // Use server-provided retry_after if available, otherwise use default - Some(retry_after.unwrap_or(Duration::from_secs(5))) - } else { - None + if !should_retry { + return Err(err.into()); } - } - LanguageModelCompletionError::ApiReadResponseError { .. } - | LanguageModelCompletionError::ApiInternalServerError { .. } - | LanguageModelCompletionError::HttpSend { .. } => { - // Exponential backoff for transient I/O and internal server errors - Some(Duration::from_secs(2_u64.pow((attempt - 1) as u32).min(30))) - } - _ => None, - }, - _ => None, - }, - }; - if let Some(retry_after) = retry_delay { - let jitter = retry_after.mul_f64(rand::thread_rng().gen_range(0.0..1.0)); - eprintln!("Attempt #{attempt}: Retry after {retry_after:?} + jitter of {jitter:?}"); - Timer::after(retry_after + jitter).await; - } else { - return response; + // Use server-provided retry_after if available, otherwise use default + let retry_after = retry_after.unwrap_or(Duration::from_secs(5)); + let jitter = retry_after.mul_f64(rand::thread_rng().gen_range(0.0..1.0)); + eprintln!( + "Attempt #{attempt}: {err}. Retry after {retry_after:?} + jitter of {jitter:?}" + ); + Timer::after(retry_after + jitter).await; + continue; + } + _ => return Err(err.into()), + }, + Err(err) => return Err(err), + }, } } } diff --git a/crates/assistant_tools/src/edit_agent/streaming_fuzzy_matcher.rs b/crates/assistant_tools/src/edit_agent/streaming_fuzzy_matcher.rs index 33b37679f0..092bdce8b3 100644 --- a/crates/assistant_tools/src/edit_agent/streaming_fuzzy_matcher.rs +++ b/crates/assistant_tools/src/edit_agent/streaming_fuzzy_matcher.rs @@ -319,7 +319,7 @@ mod tests { ); let snapshot = buffer.snapshot(); - let mut finder = StreamingFuzzyMatcher::new(snapshot); + let mut finder = StreamingFuzzyMatcher::new(snapshot.clone()); 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); + let mut finder = StreamingFuzzyMatcher::new(snapshot.clone()); // 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); + let mut finder = StreamingFuzzyMatcher::new(snapshot.clone()); // 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); + let mut finder = StreamingFuzzyMatcher::new(snapshot.clone()); // 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); + let mut finder = StreamingFuzzyMatcher::new(snapshot.clone()); // 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); + let mut finder = StreamingFuzzyMatcher::new(snapshot.clone()); 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); + let mut matcher = StreamingFuzzyMatcher::new(snapshot.clone()); 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); + let mut matcher = StreamingFuzzyMatcher::new(snapshot.clone()); // Split query into random chunks let chunks = to_random_chunks(rng, query); @@ -794,8 +794,10 @@ mod tests { fn finish(mut finder: StreamingFuzzyMatcher) -> Option { let snapshot = finder.snapshot.clone(); let matches = finder.finish(); - matches - .first() - .map(|range| snapshot.text_for_range(range.clone()).collect::()) + if let Some(range) = matches.first() { + Some(snapshot.text_for_range(range.clone()).collect::()) + } else { + None + } } } diff --git a/crates/assistant_tools/src/edit_file_tool.rs b/crates/assistant_tools/src/edit_file_tool.rs index 95b01c40eb..1c41b26092 100644 --- a/crates/assistant_tools/src/edit_file_tool.rs +++ b/crates/assistant_tools/src/edit_file_tool.rs @@ -4,11 +4,11 @@ use crate::{ schema::json_schema_for, ui::{COLLAPSED_LINES, ToolOutputPreview}, }; -use action_log::ActionLog; use agent_settings; use anyhow::{Context as _, Result, anyhow}; use assistant_tool::{ - AnyToolCard, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus, + ActionLog, AnyToolCard, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, + ToolUseStatus, }; use buffer_diff::{BufferDiff, BufferDiffSnapshot}; use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey}; @@ -155,10 +155,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) - && canonical_path.starts_with(paths::config_dir()) - { - return true; + if let Ok(canonical_path) = std::fs::canonicalize(&input.path) { + if canonical_path.starts_with(paths::config_dir()) { + return true; + } } // Check if path is inside the global config directory @@ -199,10 +199,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) - && canonical_path.starts_with(paths::config_dir()) - { - description.push_str(" (global 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)"); + } } description @@ -307,7 +307,7 @@ impl Tool for EditFileTool { let mut ambiguous_ranges = Vec::new(); while let Some(event) = events.next().await { match event { - EditAgentOutputEvent::Edited { .. } => { + EditAgentOutputEvent::Edited => { if let Some(card) = card_clone.as_ref() { card.update(cx, |card, cx| card.update_diff(cx))?; } @@ -376,7 +376,7 @@ impl Tool for EditFileTool { let output = EditFileToolOutput { original_path: project_path.path.to_path_buf(), - new_text, + new_text: new_text.clone(), old_text, raw_output: Some(agent_output), }; @@ -536,7 +536,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 +643,7 @@ impl EditFileToolCard { diff }); - self.buffer = Some(buffer); + self.buffer = Some(buffer.clone()); self.base_text = Some(base_text.into()); self.buffer_diff = Some(buffer_diff.clone()); @@ -723,13 +723,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::>(); 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,6 +776,7 @@ 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 } @@ -856,12 +857,13 @@ impl ToolCard for EditFileToolCard { ) .child( Icon::new(IconName::ArrowUpRight) - .size(IconSize::Small) + .size(IconSize::XSmall) .color(Color::Ignored), ), ) .on_click({ let path = self.path.clone(); + let workspace = workspace.clone(); move |_, window, cx| { workspace .update(cx, { @@ -1354,7 +1356,8 @@ mod tests { mode: mode.clone(), }; - cx.update(|cx| resolve_path(&input, project, cx)) + let result = cx.update(|cx| resolve_path(&input, project, cx)); + result } fn assert_resolved_path_eq(path: anyhow::Result, expected: &str) { @@ -1574,7 +1577,7 @@ mod tests { // Stream the unformatted content cx.executor().run_until_parked(); - model.send_last_completion_stream_text_chunk(UNFORMATTED_CONTENT.to_string()); + model.stream_last_completion_response(UNFORMATTED_CONTENT.to_string()); model.end_last_completion_stream(); edit_task.await @@ -1638,7 +1641,7 @@ mod tests { // Stream the unformatted content cx.executor().run_until_parked(); - model.send_last_completion_stream_text_chunk(UNFORMATTED_CONTENT.to_string()); + model.stream_last_completion_response(UNFORMATTED_CONTENT.to_string()); model.end_last_completion_stream(); edit_task.await @@ -1717,9 +1720,7 @@ mod tests { // Stream the content with trailing whitespace cx.executor().run_until_parked(); - model.send_last_completion_stream_text_chunk( - CONTENT_WITH_TRAILING_WHITESPACE.to_string(), - ); + model.stream_last_completion_response(CONTENT_WITH_TRAILING_WHITESPACE.to_string()); model.end_last_completion_stream(); edit_task.await @@ -1776,9 +1777,7 @@ mod tests { // Stream the content with trailing whitespace cx.executor().run_until_parked(); - model.send_last_completion_stream_text_chunk( - CONTENT_WITH_TRAILING_WHITESPACE.to_string(), - ); + model.stream_last_completion_response(CONTENT_WITH_TRAILING_WHITESPACE.to_string()); model.end_last_completion_stream(); edit_task.await diff --git a/crates/assistant_tools/src/fetch_tool.rs b/crates/assistant_tools/src/fetch_tool.rs index cc22c9fc09..a31ec39268 100644 --- a/crates/assistant_tools/src/fetch_tool.rs +++ b/crates/assistant_tools/src/fetch_tool.rs @@ -3,9 +3,8 @@ use std::sync::Arc; use std::{borrow::Cow, cell::RefCell}; use crate::schema::json_schema_for; -use action_log::ActionLog; use anyhow::{Context as _, Result, anyhow, bail}; -use assistant_tool::{Tool, ToolResult}; +use assistant_tool::{ActionLog, Tool, ToolResult}; use futures::AsyncReadExt as _; use gpui::{AnyWindowHandle, App, AppContext as _, Entity, Task}; use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown}; @@ -118,7 +117,7 @@ impl Tool for FetchTool { } fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity, _: &App) -> bool { - true + false } fn may_perform_edits(&self) -> bool { diff --git a/crates/assistant_tools/src/find_path_tool.rs b/crates/assistant_tools/src/find_path_tool.rs index d1451132ae..affc019417 100644 --- a/crates/assistant_tools/src/find_path_tool.rs +++ b/crates/assistant_tools/src/find_path_tool.rs @@ -1,8 +1,7 @@ use crate::{schema::json_schema_for, ui::ToolCallCardHeader}; -use action_log::ActionLog; use anyhow::{Result, anyhow}; use assistant_tool::{ - Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus, + ActionLog, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus, }; use editor::Editor; use futures::channel::oneshot::{self, Receiver}; @@ -234,7 +233,7 @@ impl ToolCard for FindPathToolCard { workspace: WeakEntity, cx: &mut Context, ) -> impl IntoElement { - let matches_label: SharedString = if self.paths.is_empty() { + let matches_label: SharedString = if self.paths.len() == 0 { "No matches".into() } else if self.paths.len() == 1 { "1 match".into() @@ -258,7 +257,7 @@ impl ToolCard for FindPathToolCard { Button::new(("path", index), button_label) .icon(IconName::ArrowUpRight) - .icon_size(IconSize::Small) + .icon_size(IconSize::XSmall) .icon_position(IconPosition::End) .label_size(LabelSize::Small) .color(Color::Muted) @@ -435,8 +434,8 @@ mod test { assert_eq!( matches, &[ - PathBuf::from(path!("root/apple/banana/carrot")), - PathBuf::from(path!("root/apple/bandana/carbonara")) + PathBuf::from("root/apple/banana/carrot"), + PathBuf::from("root/apple/bandana/carbonara") ] ); @@ -447,8 +446,8 @@ mod test { assert_eq!( matches, &[ - PathBuf::from(path!("root/apple/banana/carrot")), - PathBuf::from(path!("root/apple/bandana/carbonara")) + PathBuf::from("root/apple/banana/carrot"), + PathBuf::from("root/apple/bandana/carbonara") ] ); } diff --git a/crates/assistant_tools/src/grep_tool.rs b/crates/assistant_tools/src/grep_tool.rs index 41dde5bbfe..43c3d1d990 100644 --- a/crates/assistant_tools/src/grep_tool.rs +++ b/crates/assistant_tools/src/grep_tool.rs @@ -1,7 +1,6 @@ use crate::schema::json_schema_for; -use action_log::ActionLog; use anyhow::{Result, anyhow}; -use assistant_tool::{Tool, ToolResult}; +use assistant_tool::{ActionLog, Tool, ToolResult}; use futures::StreamExt; use gpui::{AnyWindowHandle, App, Entity, Task}; use language::{OffsetRangeExt, ParseStatus, Point}; @@ -188,14 +187,15 @@ 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) - }) - && cx.update(|cx| { + }) { + if 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?; @@ -283,11 +283,12 @@ impl Tool for GrepTool { output.extend(snapshot.text_for_range(range)); output.push_str("\n```\n"); - if let Some(ancestor_range) = ancestor_range - && end_row < ancestor_range.end.row { + if let Some(ancestor_range) = ancestor_range { + if 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; } @@ -327,7 +328,7 @@ mod tests { init_test(cx); cx.executor().allow_parking(); - let fs = FakeFs::new(cx.executor()); + let fs = FakeFs::new(cx.executor().clone()); fs.insert_tree( path!("/root"), serde_json::json!({ @@ -415,7 +416,7 @@ mod tests { init_test(cx); cx.executor().allow_parking(); - let fs = FakeFs::new(cx.executor()); + let fs = FakeFs::new(cx.executor().clone()); fs.insert_tree( path!("/root"), serde_json::json!({ @@ -494,7 +495,7 @@ mod tests { init_test(cx); cx.executor().allow_parking(); - let fs = FakeFs::new(cx.executor()); + let fs = FakeFs::new(cx.executor().clone()); // Create test file with syntax structures fs.insert_tree( @@ -892,7 +893,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" @@ -918,7 +919,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" @@ -944,7 +945,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)" @@ -969,7 +970,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)" @@ -995,7 +996,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)" @@ -1020,7 +1021,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)" @@ -1045,7 +1046,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)" @@ -1071,7 +1072,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" @@ -1098,7 +1099,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" @@ -1204,7 +1205,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!( @@ -1269,7 +1270,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!( diff --git a/crates/assistant_tools/src/list_directory_tool.rs b/crates/assistant_tools/src/list_directory_tool.rs index 5471d8923b..b1980615d6 100644 --- a/crates/assistant_tools/src/list_directory_tool.rs +++ b/crates/assistant_tools/src/list_directory_tool.rs @@ -1,7 +1,6 @@ use crate::schema::json_schema_for; -use action_log::ActionLog; use anyhow::{Result, anyhow}; -use assistant_tool::{Tool, ToolResult}; +use assistant_tool::{ActionLog, Tool, ToolResult}; use gpui::{AnyWindowHandle, App, Entity, Task}; use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; use project::{Project, WorktreeSettings}; diff --git a/crates/assistant_tools/src/move_path_tool.rs b/crates/assistant_tools/src/move_path_tool.rs index 2c065488ce..c1cbbf848d 100644 --- a/crates/assistant_tools/src/move_path_tool.rs +++ b/crates/assistant_tools/src/move_path_tool.rs @@ -1,7 +1,6 @@ use crate::schema::json_schema_for; -use action_log::ActionLog; use anyhow::{Context as _, Result, anyhow}; -use assistant_tool::{Tool, ToolResult}; +use assistant_tool::{ActionLog, Tool, ToolResult}; use gpui::{AnyWindowHandle, App, AppContext, Entity, Task}; use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; use project::Project; diff --git a/crates/assistant_tools/src/now_tool.rs b/crates/assistant_tools/src/now_tool.rs index f50ad065d1..b51b91d3d5 100644 --- a/crates/assistant_tools/src/now_tool.rs +++ b/crates/assistant_tools/src/now_tool.rs @@ -1,9 +1,8 @@ use std::sync::Arc; use crate::schema::json_schema_for; -use action_log::ActionLog; use anyhow::{Result, anyhow}; -use assistant_tool::{Tool, ToolResult}; +use assistant_tool::{ActionLog, Tool, ToolResult}; use chrono::{Local, Utc}; use gpui::{AnyWindowHandle, App, Entity, Task}; use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; diff --git a/crates/assistant_tools/src/open_tool.rs b/crates/assistant_tools/src/open_tool.rs index 6dbf66749b..8fddbb0431 100644 --- a/crates/assistant_tools/src/open_tool.rs +++ b/crates/assistant_tools/src/open_tool.rs @@ -1,7 +1,6 @@ use crate::schema::json_schema_for; -use action_log::ActionLog; use anyhow::{Context as _, Result, anyhow}; -use assistant_tool::{Tool, ToolResult}; +use assistant_tool::{ActionLog, Tool, ToolResult}; use gpui::{AnyWindowHandle, App, AppContext, Entity, Task}; use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; use project::Project; diff --git a/crates/assistant_tools/src/project_notifications_tool.rs b/crates/assistant_tools/src/project_notifications_tool.rs index e30d80207d..03487e5419 100644 --- a/crates/assistant_tools/src/project_notifications_tool.rs +++ b/crates/assistant_tools/src/project_notifications_tool.rs @@ -1,7 +1,6 @@ use crate::schema::json_schema_for; -use action_log::ActionLog; use anyhow::Result; -use assistant_tool::{Tool, ToolResult}; +use assistant_tool::{ActionLog, Tool, ToolResult}; use gpui::{AnyWindowHandle, App, Entity, Task}; use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; use project::Project; @@ -81,7 +80,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() diff --git a/crates/assistant_tools/src/read_file_tool.rs b/crates/assistant_tools/src/read_file_tool.rs index a6e984fca6..ee38273cc0 100644 --- a/crates/assistant_tools/src/read_file_tool.rs +++ b/crates/assistant_tools/src/read_file_tool.rs @@ -1,7 +1,6 @@ use crate::schema::json_schema_for; -use action_log::ActionLog; use anyhow::{Context as _, Result, anyhow}; -use assistant_tool::{Tool, ToolResult}; +use assistant_tool::{ActionLog, Tool, ToolResult}; use assistant_tool::{ToolResultContent, outline}; use gpui::{AnyWindowHandle, App, Entity, Task}; use project::{ImageItem, image_store}; @@ -68,7 +67,7 @@ impl Tool for ReadFileTool { } fn icon(&self) -> IconName { - IconName::ToolSearch + IconName::ToolRead } fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { @@ -201,7 +200,7 @@ impl Tool for ReadFileTool { buffer .file() .as_ref() - .is_none_or(|file| !file.disk_state().exists()) + .map_or(true, |file| !file.disk_state().exists()) })? { anyhow::bail!("{file_path} not found"); } @@ -287,7 +286,7 @@ impl Tool for ReadFileTool { Using the line numbers in this outline, you can call this tool again while specifying the start_line and end_line fields to see the implementations of symbols in the outline. - + Alternatively, you can fall back to the `grep` tool (if available) to search the file for specific content." } diff --git a/crates/assistant_tools/src/schema.rs b/crates/assistant_tools/src/schema.rs index dab7384efd..10a8bf0acd 100644 --- a/crates/assistant_tools/src/schema.rs +++ b/crates/assistant_tools/src/schema.rs @@ -43,11 +43,12 @@ 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, 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(); + 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(); + } + } } // oneOf is not supported, use anyOf instead diff --git a/crates/assistant_tools/src/terminal_tool.rs b/crates/assistant_tools/src/terminal_tool.rs index b28e55e78a..58833c5208 100644 --- a/crates/assistant_tools/src/terminal_tool.rs +++ b/crates/assistant_tools/src/terminal_tool.rs @@ -2,10 +2,9 @@ use crate::{ schema::json_schema_for, ui::{COLLAPSED_LINES, ToolOutputPreview}, }; -use action_log::ActionLog; use agent_settings; use anyhow::{Context as _, Result, anyhow}; -use assistant_tool::{Tool, ToolCard, ToolResult, ToolUseStatus}; +use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus}; use futures::{FutureExt as _, future::Shared}; use gpui::{ Animation, AnimationExt, AnyWindowHandle, App, AppContext, Empty, Entity, EntityId, Task, @@ -59,9 +58,12 @@ impl TerminalTool { } if which::which("bash").is_ok() { + log::info!("agent selected bash for terminal tool"); "bash".into() } else { - get_system_shell() + let shell = get_system_shell(); + log::info!("agent selected {shell} for terminal tool"); + shell } }); Self { @@ -102,7 +104,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 @@ -213,8 +215,7 @@ impl Tool for TerminalTool { async move |cx| { let program = program.await; let env = env.await; - - project + let terminal = project .update(cx, |project, cx| { project.create_terminal( TerminalKind::Task(task::SpawnInTerminal { @@ -224,10 +225,12 @@ impl Tool for TerminalTool { env, ..Default::default() }), + window, cx, ) })? - .await + .await; + terminal } }); @@ -350,7 +353,7 @@ fn process_content( if is_empty { "Command executed successfully.".to_string() } else { - content + content.to_string() } } Some(exit_status) => { @@ -384,7 +387,7 @@ fn working_dir( let project = project.read(cx); let cd = &input.cd; - if cd == "." || cd.is_empty() { + if cd == "." || cd == "" { // Accept "." or "" as meaning "the one worktree" if we only have one worktree. let mut worktrees = project.worktrees(cx); @@ -409,8 +412,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."); diff --git a/crates/assistant_tools/src/thinking_tool.rs b/crates/assistant_tools/src/thinking_tool.rs index 17ce4afc2e..443c2930be 100644 --- a/crates/assistant_tools/src/thinking_tool.rs +++ b/crates/assistant_tools/src/thinking_tool.rs @@ -1,9 +1,8 @@ use std::sync::Arc; use crate::schema::json_schema_for; -use action_log::ActionLog; use anyhow::{Result, anyhow}; -use assistant_tool::{Tool, ToolResult}; +use assistant_tool::{ActionLog, Tool, ToolResult}; use gpui::{AnyWindowHandle, App, Entity, Task}; use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; use project::Project; @@ -38,7 +37,7 @@ impl Tool for ThinkingTool { } fn icon(&self) -> IconName { - IconName::ToolThink + IconName::ToolBulb } fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { diff --git a/crates/assistant_tools/src/ui/tool_call_card_header.rs b/crates/assistant_tools/src/ui/tool_call_card_header.rs index b41f19432f..b71453373f 100644 --- a/crates/assistant_tools/src/ui/tool_call_card_header.rs +++ b/crates/assistant_tools/src/ui/tool_call_card_header.rs @@ -101,11 +101,14 @@ impl RenderOnce for ToolCallCardHeader { }) .when_some(secondary_text, |this, secondary_text| { this.child(bullet_divider()) - .child(div().text_size(font_size).child(secondary_text)) + .child(div().text_size(font_size).child(secondary_text.clone())) }) .when_some(code_path, |this, code_path| { - this.child(bullet_divider()) - .child(Label::new(code_path).size(LabelSize::Small).inline_code(cx)) + this.child(bullet_divider()).child( + Label::new(code_path.clone()) + .size(LabelSize::Small) + .inline_code(cx), + ) }) .with_animation( "loading-label", diff --git a/crates/assistant_tools/src/web_search_tool.rs b/crates/assistant_tools/src/web_search_tool.rs index dbcca0a1f6..d4a12f22c5 100644 --- a/crates/assistant_tools/src/web_search_tool.rs +++ b/crates/assistant_tools/src/web_search_tool.rs @@ -2,10 +2,9 @@ use std::{sync::Arc, time::Duration}; use crate::schema::json_schema_for; use crate::ui::ToolCallCardHeader; -use action_log::ActionLog; use anyhow::{Context as _, Result, anyhow}; use assistant_tool::{ - Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus, + ActionLog, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus, }; use cloud_llm_client::{WebSearchResponse, WebSearchResult}; use futures::{Future, FutureExt, TryFutureExt}; @@ -46,7 +45,7 @@ impl Tool for WebSearchTool { } fn icon(&self) -> IconName { - IconName::ToolWeb + IconName::Globe } fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { @@ -178,7 +177,7 @@ impl ToolCard for WebSearchToolCard { .label_size(LabelSize::Small) .color(Color::Muted) .icon(IconName::ArrowUpRight) - .icon_size(IconSize::Small) + .icon_size(IconSize::XSmall) .icon_position(IconPosition::End) .truncate(true) .tooltip({ @@ -193,7 +192,10 @@ impl ToolCard for WebSearchToolCard { ) } }) - .on_click(move |_, _, cx| cx.open_url(&url)) + .on_click({ + let url = url.clone(); + move |_, _, cx| cx.open_url(&url) + }) })) .into_any(), ), diff --git a/crates/audio/Cargo.toml b/crates/audio/Cargo.toml index ae7eb52fd3..d857a3eb2f 100644 --- a/crates/audio/Cargo.toml +++ b/crates/audio/Cargo.toml @@ -15,10 +15,9 @@ doctest = false [dependencies] anyhow.workspace = true collections.workspace = true +derive_more.workspace = true gpui.workspace = true -settings.workspace = true -schemars.workspace = true -serde.workspace = true -rodio = { workspace = true, features = [ "wav", "playback", "tracing" ] } +parking_lot.workspace = true +rodio = { version = "0.21.1", default-features = false, features = ["wav", "playback", "tracing"] } util.workspace = true workspace-hack.workspace = true diff --git a/crates/audio/src/assets.rs b/crates/audio/src/assets.rs new file mode 100644 index 0000000000..fd5c935d87 --- /dev/null +++ b/crates/audio/src/assets.rs @@ -0,0 +1,54 @@ +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>>>; + +pub struct SoundRegistry { + cache: Arc>>, + assets: Box, +} + +struct GlobalSoundRegistry(Arc); + +impl Global for GlobalSoundRegistry {} + +impl SoundRegistry { + pub fn new(source: impl AssetSource) -> Arc { + Arc::new(Self { + cache: Default::default(), + assets: Box::new(source), + }) + } + + pub fn global(cx: &App) -> Arc { + cx.global::().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 + 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) + } +} diff --git a/crates/audio/src/audio.rs b/crates/audio/src/audio.rs index b4f2c24fef..44baa16aa2 100644 --- a/crates/audio/src/audio.rs +++ b/crates/audio/src/audio.rs @@ -1,19 +1,16 @@ -use anyhow::{Context as _, Result, anyhow}; -use collections::HashMap; -use gpui::{App, BorrowAppContext, Global}; -use rodio::{Decoder, OutputStream, OutputStreamBuilder, Source, source::Buffered}; -use settings::Settings; -use std::io::Cursor; +use assets::SoundRegistry; +use derive_more::{Deref, DerefMut}; +use gpui::{App, AssetSource, BorrowAppContext, Global}; +use rodio::{OutputStream, OutputStreamBuilder}; use util::ResultExt; -mod audio_settings; -pub use audio_settings::AudioSettings; +mod assets; -pub fn init(cx: &mut App) { - AudioSettings::register(cx); +pub fn init(source: impl AssetSource, cx: &mut App) { + SoundRegistry::set_global(source, cx); + cx.set_global(GlobalAudio(Audio::new())); } -#[derive(Copy, Clone, Eq, Hash, PartialEq)] pub enum Sound { Joined, Leave, @@ -41,12 +38,18 @@ impl Sound { #[derive(Default)] pub struct Audio { output_handle: Option, - source_cache: HashMap>>>>, } -impl Global for Audio {} +#[derive(Deref, DerefMut)] +struct GlobalAudio(Audio); + +impl Global for GlobalAudio {} impl Audio { + pub fn new() -> Self { + Self::default() + } + fn ensure_output_exists(&mut self) -> Option<&OutputStream> { if self.output_handle.is_none() { self.output_handle = OutputStreamBuilder::open_default_stream().log_err(); @@ -55,51 +58,26 @@ impl Audio { self.output_handle.as_ref() } - pub fn play_source( - source: impl rodio::Source + Send + 'static, - cx: &mut App, - ) -> anyhow::Result<()> { - cx.update_default_global(|this: &mut Self, _cx| { - let output_handle = this - .ensure_output_exists() - .ok_or_else(|| anyhow!("Could not open audio output"))?; - output_handle.mixer().add(source); - Ok(()) - }) - } - pub fn play_sound(sound: Sound, cx: &mut App) { - cx.update_default_global(|this: &mut Self, cx| { - let source = this.sound_source(sound, cx).log_err()?; + if !cx.has_global::() { + return; + } + + cx.update_global::(|this, cx| { let output_handle = this.ensure_output_exists()?; + let source = SoundRegistry::global(cx).get(sound.file()).log_err()?; output_handle.mixer().add(source); Some(()) }); } pub fn end_call(cx: &mut App) { - cx.update_default_global(|this: &mut Self, _cx| { + if !cx.has_global::() { + return; + } + + cx.update_global::(|this, _| { this.output_handle.take(); }); } - - fn sound_source(&mut self, sound: Sound, cx: &App) -> Result> { - 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) - } } diff --git a/crates/audio/src/audio_settings.rs b/crates/audio/src/audio_settings.rs deleted file mode 100644 index 807179881c..0000000000 --- a/crates/audio/src/audio_settings.rs +++ /dev/null @@ -1,33 +0,0 @@ -use anyhow::Result; -use gpui::App; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; - -#[derive(Deserialize, Debug)] -pub struct AudioSettings { - /// Opt into the new audio system. - #[serde(rename = "experimental.rodio_audio", default)] - pub rodio_audio: bool, // default is false -} - -/// Configuration of audio in Zed. -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] -#[serde(default)] -pub struct AudioSettingsContent { - /// Whether to use the experimental audio system - #[serde(rename = "experimental.rodio_audio", default)] - pub rodio_audio: bool, -} - -impl Settings for AudioSettings { - const KEY: Option<&'static str> = Some("audio"); - - type FileContent = AudioSettingsContent; - - fn load(sources: SettingsSources, _cx: &mut App) -> Result { - sources.json_merge() - } - - fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} -} diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 2150873cad..d62a9cdbe3 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -59,9 +59,16 @@ pub enum VersionCheckType { pub enum AutoUpdateStatus { Idle, Checking, - Downloading { version: VersionCheckType }, - Installing { version: VersionCheckType }, - Updated { version: VersionCheckType }, + Downloading { + version: VersionCheckType, + }, + Installing { + version: VersionCheckType, + }, + Updated { + binary_path: PathBuf, + version: VersionCheckType, + }, Errored, } @@ -76,7 +83,6 @@ pub struct AutoUpdater { current_version: SemanticVersion, http_client: Arc, pending_poll: Option>>, - quit_subscription: Option, } #[derive(Deserialize, Clone, Debug)] @@ -128,15 +134,10 @@ impl Settings for AutoUpdateSetting { type FileContent = Option; fn load(sources: SettingsSources, _: &mut App) -> Result { - let auto_update = [ - sources.server, - sources.release_channel, - sources.operating_system, - sources.user, - ] - .into_iter() - .find_map(|value| value.copied().flatten()) - .unwrap_or(sources.default.ok_or_else(Self::missing_default)?); + let auto_update = [sources.server, sources.release_channel, sources.user] + .into_iter() + .find_map(|value| value.copied().flatten()) + .unwrap_or(sources.default.ok_or_else(Self::missing_default)?); Ok(Self(auto_update.0)) } @@ -158,7 +159,7 @@ pub fn init(http_client: Arc, cx: &mut App) { AutoUpdateSetting::register(cx); cx.observe_new(|workspace: &mut Workspace, _window, _cx| { - workspace.register_action(|_, action, window, cx| check(action, window, cx)); + workspace.register_action(|_, action: &Check, window, cx| check(action, window, cx)); workspace.register_action(|_, action, _, cx| { view_release_notes(action, cx); @@ -168,7 +169,7 @@ pub fn init(http_client: Arc, cx: &mut App) { let version = release_channel::AppVersion::global(cx); let auto_updater = cx.new(|cx| { - let updater = AutoUpdater::new(version, http_client, cx); + let updater = AutoUpdater::new(version, http_client); let poll_for_updates = ReleaseChannel::try_global(cx) .map(|channel| channel.poll_for_updates()) @@ -315,34 +316,12 @@ impl AutoUpdater { cx.default_global::().0.clone() } - fn new( - current_version: SemanticVersion, - http_client: Arc, - cx: &mut Context, - ) -> Self { - // On windows, executable files cannot be overwritten while they are - // running, so we must wait to overwrite the application until quitting - // or restarting. When quitting the app, we spawn the auto update helper - // to finish the auto update process after Zed exits. When restarting - // the app after an update, we use `set_restart_path` to run the auto - // update helper instead of the app, so that it can overwrite the app - // and then spawn the new binary. - let quit_subscription = Some(cx.on_app_quit(|_, _| async move { - #[cfg(target_os = "windows")] - finalize_auto_update_on_quit(); - })); - - cx.on_app_restart(|this, _| { - this.quit_subscription.take(); - }) - .detach(); - + fn new(current_version: SemanticVersion, http_client: Arc) -> Self { Self { status: AutoUpdateStatus::Idle, current_version, http_client, pending_poll: None, - quit_subscription, } } @@ -543,7 +522,7 @@ impl AutoUpdater { async fn update(this: Entity, mut cx: AsyncApp) -> Result<()> { let (client, installed_version, previous_status, release_channel) = - this.read_with(&cx, |this, cx| { + this.read_with(&mut cx, |this, cx| { ( this.http_client.clone(), this.current_version, @@ -552,8 +531,6 @@ impl AutoUpdater { ) })?; - Self::check_dependencies()?; - this.update(&mut cx, |this, cx| { this.status = AutoUpdateStatus::Checking; cx.notify(); @@ -600,15 +577,13 @@ impl AutoUpdater { cx.notify(); })?; - let new_binary_path = Self::install_release(installer_dir, target_path, &cx).await?; - if let Some(new_binary_path) = new_binary_path { - cx.update(|cx| cx.set_restart_path(new_binary_path))?; - } + let binary_path = Self::binary_path(installer_dir, target_path, &cx).await?; this.update(&mut cx, |this, cx| { this.set_should_show_update_notification(true, cx) .detach_and_log_err(cx); this.status = AutoUpdateStatus::Updated { + binary_path, version: newer_version, }; cx.notify(); @@ -659,15 +634,6 @@ impl AutoUpdater { } } - fn check_dependencies() -> Result<()> { - #[cfg(not(target_os = "windows"))] - anyhow::ensure!( - which::which("rsync").is_ok(), - "Aborting. Could not find rsync which is required for auto-updates." - ); - Ok(()) - } - async fn target_path(installer_dir: &InstallerDir) -> Result { let filename = match OS { "macos" => anyhow::Ok("Zed.dmg"), @@ -676,14 +642,20 @@ impl AutoUpdater { unsupported_os => anyhow::bail!("not supported: {unsupported_os}"), }?; + #[cfg(not(target_os = "windows"))] + anyhow::ensure!( + which::which("rsync").is_ok(), + "Aborting. Could not find rsync which is required for auto-updates." + ); + Ok(installer_dir.path().join(filename)) } - async fn install_release( + async fn binary_path( installer_dir: InstallerDir, target_path: PathBuf, cx: &AsyncApp, - ) -> Result> { + ) -> Result { match OS { "macos" => install_release_macos(&installer_dir, target_path, cx).await, "linux" => install_release_linux(&installer_dir, target_path, cx).await, @@ -824,7 +796,7 @@ async fn install_release_linux( temp_dir: &InstallerDir, downloaded_tar_gz: PathBuf, cx: &AsyncApp, -) -> Result> { +) -> Result { let channel = cx.update(|cx| ReleaseChannel::global(cx).dev_name())?; let home_dir = PathBuf::from(env::var("HOME").context("no HOME env var set")?); let running_app_path = cx.update(|cx| cx.app_path())??; @@ -884,14 +856,14 @@ async fn install_release_linux( String::from_utf8_lossy(&output.stderr) ); - Ok(Some(to.join(expected_suffix))) + Ok(to.join(expected_suffix)) } async fn install_release_macos( temp_dir: &InstallerDir, downloaded_dmg: PathBuf, cx: &AsyncApp, -) -> Result> { +) -> Result { let running_app_path = cx.update(|cx| cx.app_path())??; let running_app_filename = running_app_path .file_name() @@ -933,10 +905,10 @@ async fn install_release_macos( String::from_utf8_lossy(&output.stderr) ); - Ok(None) + Ok(running_app_path) } -async fn install_release_windows(downloaded_installer: PathBuf) -> Result> { +async fn install_release_windows(downloaded_installer: PathBuf) -> Result { let output = Command::new(downloaded_installer) .arg("/verysilent") .arg("/update=true") @@ -949,36 +921,29 @@ async fn install_release_windows(downloaded_installer: PathBuf) -> Result
{ - div() - .occlude() - .id("breakpoint-list-vertical-scrollbar") - .on_mouse_move(cx.listener(|_, _, _, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|_, _, _, cx| { + fn render_vertical_scrollbar(&self, cx: &mut Context) -> Option> { + if !(self.show_scrollbar || self.scrollbar_state.is_dragging()) { + return None; + } + Some( + div() + .occlude() + .id("breakpoint-list-vertical-scrollbar") + .on_mouse_move(cx.listener(|_, _, _, cx| { + cx.notify(); + cx.stop_propagation() + })) + .on_hover(|_, _, cx| { cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _, cx| { - cx.notify(); - })) - .h_full() - .absolute() - .right_1() - .top_1() - .bottom_0() - .w(px(12.)) - .cursor_default() - .children(Scrollbar::vertical(self.scrollbar_state.clone()).map(|s| s.auto_hide(cx))) + }) + .on_any_mouse_down(|_, _, cx| { + cx.stop_propagation(); + }) + .on_mouse_up( + MouseButton::Left, + cx.listener(|_, _, _, cx| { + cx.stop_propagation(); + }), + ) + .on_scroll_wheel(cx.listener(|_, _, _, cx| { + cx.notify(); + })) + .h_full() + .absolute() + .right_1() + .top_1() + .bottom_0() + .w(px(12.)) + .cursor_default() + .children(Scrollbar::vertical(self.scrollbar_state.clone())), + ) } - pub(crate) fn render_control_strip(&self) -> AnyElement { let selection_kind = self.selection_kind(); let focus_handle = self.focus_handle.clone(); - let remove_breakpoint_tooltip = selection_kind.map(|(kind, _)| match kind { SelectedBreakpointKind::Source => "Remove breakpoint from a breakpoint list", SelectedBreakpointKind::Exception => { @@ -620,7 +661,6 @@ impl BreakpointList { } SelectedBreakpointKind::Data => "Remove data breakpoint from a breakpoint list", }); - let toggle_label = selection_kind.map(|(_, is_enabled)| { if is_enabled { ( @@ -633,12 +673,13 @@ impl BreakpointList { }); h_flex() + .gap_2() .child( IconButton::new( "disable-breakpoint-breakpoint-list", IconName::DebugDisabledBreakpoint, ) - .icon_size(IconSize::Small) + .icon_size(IconSize::XSmall) .when_some(toggle_label, |this, (label, meta)| { this.tooltip({ let focus_handle = focus_handle.clone(); @@ -664,8 +705,9 @@ impl BreakpointList { }), ) .child( - IconButton::new("remove-breakpoint-breakpoint-list", IconName::Trash) - .icon_size(IconSize::Small) + IconButton::new("remove-breakpoint-breakpoint-list", IconName::X) + .icon_size(IconSize::XSmall) + .icon_color(ui::Color::Error) .when_some(remove_breakpoint_tooltip, |this, tooltip| { this.tooltip({ let focus_handle = focus_handle.clone(); @@ -685,12 +727,14 @@ 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) } }), ) + .mr_2() .into_any_element() } } @@ -771,11 +815,19 @@ impl Render for BreakpointList { .chain(data_breakpoints) .chain(exception_breakpoints), ); - v_flex() .id("breakpoint-list") .key_context("BreakpointList") .track_focus(&self.focus_handle) + .on_hover(cx.listener(|this, hovered, window, cx| { + if *hovered { + this.show_scrollbar = true; + this.hide_scrollbar_task.take(); + cx.notify(); + } else if !this.focus_handle.contains_focused(window, cx) { + this.hide_scrollbar(window, cx); + } + })) .on_action(cx.listener(Self::select_next)) .on_action(cx.listener(Self::select_previous)) .on_action(cx.listener(Self::select_first)) @@ -787,33 +839,35 @@ impl Render for BreakpointList { .on_action(cx.listener(Self::next_breakpoint_property)) .on_action(cx.listener(Self::previous_breakpoint_property)) .size_full() - .pt_1() - .child(self.render_list(cx)) - .child(self.render_vertical_scrollbar(cx)) + .m_0p5() + .child( + v_flex() + .size_full() + .child(self.render_list(cx)) + .children(self.render_vertical_scrollbar(cx)), + ) .when_some(self.strip_mode, |this, _| { - this.child(Divider::horizontal().color(DividerColor::Border)) - .child( - h_flex() - .p_1() - .rounded_sm() - .bg(cx.theme().colors().editor_background) - .border_1() - .when( - self.input.focus_handle(cx).contains_focused(window, cx), - |this| { - let colors = cx.theme().colors(); - - let border_color = if self.input.read(cx).read_only(cx) { - colors.border_disabled - } else { - colors.border_transparent - }; - - this.border_color(border_color) - }, - ) - .child(self.input.clone()), - ) + this.child(Divider::horizontal()).child( + h_flex() + // .w_full() + .m_0p5() + .p_0p5() + .border_1() + .rounded_sm() + .when( + self.input.focus_handle(cx).contains_focused(window, cx), + |this| { + let colors = cx.theme().colors(); + let border = if self.input.read(cx).read_only(cx) { + colors.border_disabled + } else { + colors.border_focused + }; + this.border_color(border) + }, + ) + .child(self.input.clone()), + ) }) } } @@ -844,17 +898,12 @@ impl LineBreakpoint { let path = self.breakpoint.path.clone(); let row = self.breakpoint.row; let is_enabled = self.breakpoint.state.is_enabled(); - let indicator = div() .id(SharedString::from(format!( "breakpoint-ui-toggle-{:?}/{}:{}", self.dir, self.name, self.line ))) - .child( - Icon::new(icon_name) - .color(Color::Debugger) - .size(IconSize::XSmall), - ) + .cursor_pointer() .tooltip({ let focus_handle = focus_handle.clone(); move |window, cx| { @@ -886,14 +935,17 @@ impl LineBreakpoint { .ok(); } }) + .child( + Icon::new(icon_name) + .color(Color::Debugger) + .size(IconSize::XSmall), + ) .on_mouse_down(MouseButton::Left, move |_, _, _| {}); ListItem::new(SharedString::from(format!( "breakpoint-ui-item-{:?}/{}:{}", self.dir, self.name, self.line ))) - .toggle_state(is_selected) - .inset(true) .on_click({ let weak = weak.clone(); move |_, window, cx| { @@ -903,20 +955,23 @@ impl LineBreakpoint { .ok(); } }) + .start_slot(indicator) + .rounded() .on_secondary_mouse_down(|_, _, cx| { cx.stop_propagation(); }) - .start_slot(indicator) .child( h_flex() + .w_full() + .mr_4() + .py_0p5() + .gap_1() + .min_h(px(26.)) + .justify_between() .id(SharedString::from(format!( "breakpoint-ui-on-click-go-to-line-{:?}/{}:{}", self.dir, self.name, self.line ))) - .w_full() - .gap_1() - .min_h(rems_from_px(26.)) - .justify_between() .on_click({ let weak = weak.clone(); move |_, window, cx| { @@ -927,9 +982,9 @@ impl LineBreakpoint { .ok(); } }) + .cursor_pointer() .child( h_flex() - .id("label-container") .gap_0p5() .child( Label::new(format!("{}:{}", self.name, self.line)) @@ -949,18 +1004,16 @@ impl LineBreakpoint { .line_height_style(ui::LineHeightStyle::UiLabel) .truncate(), ) - })) - .when_some(self.dir.as_ref(), |this, parent_dir| { - this.tooltip(Tooltip::text(format!( - "Worktree parent path: {parent_dir}" - ))) - }), + })), ) + .when_some(self.dir.as_ref(), |this, parent_dir| { + this.tooltip(Tooltip::text(format!("Worktree parent path: {parent_dir}"))) + }) .child(BreakpointOptionsStrip { props, breakpoint: BreakpointEntry { kind: BreakpointEntryKind::LineBreakpoint(self.clone()), - weak, + weak: weak, }, is_selected, focus_handle, @@ -968,16 +1021,15 @@ impl LineBreakpoint { index: ix, }), ) + .toggle_state(is_selected) } } - #[derive(Clone, Debug)] struct ExceptionBreakpoint { id: String, data: ExceptionBreakpointsFilter, is_enabled: bool, } - #[derive(Clone, Debug)] struct DataBreakpoint(project::debugger::session::DataBreakpointState); @@ -998,24 +1050,17 @@ impl DataBreakpoint { }; let is_enabled = self.0.is_enabled; let id = self.0.dap.data_id.clone(); - ListItem::new(SharedString::from(format!( "data-breakpoint-ui-item-{}", self.0.dap.data_id ))) - .toggle_state(is_selected) - .inset(true) + .rounded() .start_slot( div() .id(SharedString::from(format!( "data-breakpoint-ui-item-{}-click-handler", self.0.dap.data_id ))) - .child( - Icon::new(IconName::Binary) - .color(color) - .size(IconSize::Small), - ) .tooltip({ let focus_handle = focus_handle.clone(); move |window, cx| { @@ -1040,18 +1085,25 @@ impl DataBreakpoint { }) .ok(); } - }), + }) + .cursor_pointer() + .child( + Icon::new(IconName::Binary) + .color(color) + .size(IconSize::Small), + ), ) .child( h_flex() .w_full() - .gap_1() - .min_h(rems_from_px(26.)) + .mr_4() + .py_0p5() .justify_between() .child( v_flex() .py_1() .gap_1() + .min_h(px(26.)) .justify_center() .id(("data-breakpoint-label", ix)) .child( @@ -1072,6 +1124,7 @@ impl DataBreakpoint { index: ix, }), ) + .toggle_state(is_selected) } } @@ -1093,13 +1146,10 @@ impl ExceptionBreakpoint { let id = SharedString::from(&self.id); let is_enabled = self.is_enabled; let weak = list.clone(); - ListItem::new(SharedString::from(format!( "exception-breakpoint-ui-item-{}", self.id ))) - .toggle_state(is_selected) - .inset(true) .on_click({ let list = list.clone(); move |_, window, cx| { @@ -1107,6 +1157,7 @@ impl ExceptionBreakpoint { .ok(); } }) + .rounded() .on_secondary_mouse_down(|_, _, cx| { cx.stop_propagation(); }) @@ -1116,11 +1167,6 @@ impl ExceptionBreakpoint { "exception-breakpoint-ui-item-{}-click-handler", self.id ))) - .child( - Icon::new(IconName::Flame) - .color(color) - .size(IconSize::Small), - ) .tooltip({ let focus_handle = focus_handle.clone(); move |window, cx| { @@ -1138,24 +1184,32 @@ impl ExceptionBreakpoint { } }) .on_click({ + let list = list.clone(); move |_, _, cx| { list.update(cx, |this, cx| { this.toggle_exception_breakpoint(&id, cx); }) .ok(); } - }), + }) + .cursor_pointer() + .child( + Icon::new(IconName::Flame) + .color(color) + .size(IconSize::Small), + ), ) .child( h_flex() .w_full() - .gap_1() - .min_h(rems_from_px(26.)) + .mr_4() + .py_0p5() .justify_between() .child( v_flex() .py_1() .gap_1() + .min_h(px(26.)) .justify_center() .id(("exception-breakpoint-label", ix)) .child( @@ -1171,7 +1225,7 @@ impl ExceptionBreakpoint { props, breakpoint: BreakpointEntry { kind: BreakpointEntryKind::ExceptionBreakpoint(self.clone()), - weak, + weak: weak, }, is_selected, focus_handle, @@ -1179,6 +1233,7 @@ impl ExceptionBreakpoint { index: ix, }), ) + .toggle_state(is_selected) } } #[derive(Clone, Debug)] @@ -1280,7 +1335,6 @@ impl BreakpointEntry { } } } - bitflags::bitflags! { #[derive(Clone, Copy)] pub struct SupportedBreakpointProperties: u32 { @@ -1339,7 +1393,6 @@ impl BreakpointOptionsStrip { fn is_toggled(&self, expected_mode: ActiveBreakpointStripMode) -> bool { self.is_selected && self.strip_mode == Some(expected_mode) } - fn on_click_callback( &self, mode: ActiveBreakpointStripMode, @@ -1359,8 +1412,7 @@ impl BreakpointOptionsStrip { .ok(); } } - - fn add_focus_styles( + fn add_border( &self, kind: ActiveBreakpointStripMode, available: bool, @@ -1369,25 +1421,22 @@ impl BreakpointOptionsStrip { ) -> impl Fn(Div) -> Div { move |this: Div| { // Avoid layout shifts in case there's no colored border - let this = this.border_1().rounded_sm(); - let color = cx.theme().colors(); - + let this = this.border_2().rounded_sm(); if self.is_selected && self.strip_mode == Some(kind) { + let theme = cx.theme().colors(); if self.focus_handle.is_focused(window) { - this.bg(color.editor_background) - .border_color(color.border_focused) + this.border_color(theme.border_selected) } else { - this.border_color(color.border) + this.border_color(theme.border_disabled) } } else if !available { - this.border_color(color.border_transparent) + this.border_color(cx.theme().colors().border_disabled) } else { this } } } } - impl RenderOnce for BreakpointOptionsStrip { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { let id = self.breakpoint.id(); @@ -1410,117 +1459,73 @@ impl RenderOnce for BreakpointOptionsStrip { }; let color_for_toggle = |is_enabled| { if is_enabled { - Color::Default + ui::Color::Default } else { - Color::Muted + ui::Color::Muted } }; h_flex() - .gap_px() - .mr_3() // Space to avoid overlapping with the scrollbar + .gap_1() .child( - div() - .map(self.add_focus_styles( - ActiveBreakpointStripMode::Log, - supports_logs, - window, - cx, - )) + div().map(self.add_border(ActiveBreakpointStripMode::Log, supports_logs, window, cx)) .child( IconButton::new( SharedString::from(format!("{id}-log-toggle")), - IconName::Notepad, + IconName::ScrollText, ) - .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::XSmall) .style(style_for_toggle(ActiveBreakpointStripMode::Log, has_logs)) - .icon_size(IconSize::Small) .icon_color(color_for_toggle(has_logs)) - .when(has_logs, |this| this.indicator(Indicator::dot().color(Color::Info))) .disabled(!supports_logs) .toggle_state(self.is_toggled(ActiveBreakpointStripMode::Log)) - .on_click(self.on_click_callback(ActiveBreakpointStripMode::Log)) - .tooltip(|window, cx| { - Tooltip::with_meta( - "Set Log Message", - None, - "Set log message to display (instead of stopping) when a breakpoint is hit.", - window, - cx, - ) - }), + .on_click(self.on_click_callback(ActiveBreakpointStripMode::Log)).tooltip(|window, cx| Tooltip::with_meta("Set Log Message", None, "Set log message to display (instead of stopping) when a breakpoint is hit", window, cx)) ) .when(!has_logs && !self.is_selected, |this| this.invisible()), ) .child( - div() - .map(self.add_focus_styles( - ActiveBreakpointStripMode::Condition, - supports_condition, - window, - cx, - )) + div().map(self.add_border( + ActiveBreakpointStripMode::Condition, + supports_condition, + window, cx + )) .child( IconButton::new( SharedString::from(format!("{id}-condition-toggle")), IconName::SplitAlt, ) - .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::XSmall) .style(style_for_toggle( ActiveBreakpointStripMode::Condition, - has_condition, + has_condition )) - .icon_size(IconSize::Small) .icon_color(color_for_toggle(has_condition)) - .when(has_condition, |this| this.indicator(Indicator::dot().color(Color::Info))) .disabled(!supports_condition) .toggle_state(self.is_toggled(ActiveBreakpointStripMode::Condition)) .on_click(self.on_click_callback(ActiveBreakpointStripMode::Condition)) - .tooltip(|window, cx| { - Tooltip::with_meta( - "Set Condition", - None, - "Set condition to evaluate when a breakpoint is hit. Program execution will stop only when the condition is met.", - window, - cx, - ) - }), + .tooltip(|window, cx| Tooltip::with_meta("Set Condition", None, "Set condition to evaluate when a breakpoint is hit. Program execution will stop only when the condition is met", window, cx)) ) .when(!has_condition && !self.is_selected, |this| this.invisible()), ) .child( - div() - .map(self.add_focus_styles( - ActiveBreakpointStripMode::HitCondition, - supports_hit_condition, - window, - cx, - )) + div().map(self.add_border( + ActiveBreakpointStripMode::HitCondition, + supports_hit_condition,window, cx + )) .child( IconButton::new( SharedString::from(format!("{id}-hit-condition-toggle")), IconName::ArrowDown10, ) + .icon_size(IconSize::XSmall) .style(style_for_toggle( ActiveBreakpointStripMode::HitCondition, has_hit_condition, )) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::Small) .icon_color(color_for_toggle(has_hit_condition)) - .when(has_hit_condition, |this| this.indicator(Indicator::dot().color(Color::Info))) .disabled(!supports_hit_condition) .toggle_state(self.is_toggled(ActiveBreakpointStripMode::HitCondition)) - .on_click(self.on_click_callback(ActiveBreakpointStripMode::HitCondition)) - .tooltip(|window, cx| { - Tooltip::with_meta( - "Set Hit Condition", - None, - "Set expression that controls how many hits of the breakpoint are ignored.", - window, - cx, - ) - }), + .on_click(self.on_click_callback(ActiveBreakpointStripMode::HitCondition)).tooltip(|window, cx| Tooltip::with_meta("Set Hit Condition", None, "Set expression that controls how many hits of the breakpoint are ignored.", window, cx)) ) .when(!has_hit_condition && !self.is_selected, |this| { this.invisible() diff --git a/crates/debugger_ui/src/session/running/console.rs b/crates/debugger_ui/src/session/running/console.rs index a801cedd26..1385bec54e 100644 --- a/crates/debugger_ui/src/session/running/console.rs +++ b/crates/debugger_ui/src/session/running/console.rs @@ -352,7 +352,7 @@ impl Console { .child( div() .px_1() - .child(Icon::new(IconName::ChevronDown).size(IconSize::XSmall)), + .child(Icon::new(IconName::ChevronDownSmall).size(IconSize::XSmall)), ), ) .when( @@ -365,9 +365,9 @@ impl Console { Some(ContextMenu::build(window, cx, |context_menu, _, _| { context_menu .when_some(keybinding_target.clone(), |el, keybinding_target| { - el.context(keybinding_target) + el.context(keybinding_target.clone()) }) - .action("Watch Expression", WatchExpression.boxed_clone()) + .action("Watch expression", WatchExpression.boxed_clone()) })) }) }, @@ -452,22 +452,18 @@ impl Render for Console { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let query_focus_handle = self.query_bar.focus_handle(cx); self.update_output(window, cx); - v_flex() .track_focus(&self.focus_handle) .key_context("DebugConsole") .on_action(cx.listener(Self::evaluate)) .on_action(cx.listener(Self::watch_expression)) .size_full() - .border_2() - .bg(cx.theme().colors().editor_background) .child(self.render_console(cx)) .when(self.is_running(cx), |this| { this.child(Divider::horizontal()).child( h_flex() .on_action(cx.listener(Self::previous_query)) .on_action(cx.listener(Self::next_query)) - .p_1() .gap_1() .bg(cx.theme().colors().editor_background) .child(self.render_query_bar(cx)) @@ -478,9 +474,6 @@ impl Render for Console { .on_click(move |_, window, cx| { window.dispatch_action(Box::new(Confirm), cx) }) - .layer(ui::ElevationIndex::ModalSurface) - .size(ui::ButtonSize::Compact) - .child(Label::new("Evaluate")) .tooltip({ let query_focus_handle = query_focus_handle.clone(); @@ -493,7 +486,10 @@ impl Render for Console { cx, ) } - }), + }) + .layer(ui::ElevationIndex::ModalSurface) + .size(ui::ButtonSize::Compact) + .child(Label::new("Evaluate")), self.render_submit_menu( ElementId::Name("split-button-right-confirm-button".into()), Some(query_focus_handle.clone()), @@ -503,6 +499,7 @@ impl Render for Console { )), ) }) + .border_2() } } @@ -611,16 +608,17 @@ 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 - && variables + if let Some(evaluate_name) = &variable.evaluate_name { + if 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 @@ -696,7 +694,7 @@ impl ConsoleQueryBarCompletionProvider { new_bytes: &[u8], snapshot: &TextBufferSnapshot, ) -> Range { - 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; @@ -976,7 +974,7 @@ mod tests { &cx.buffer_text(), snapshot.anchor_before(buffer_position), replacement.as_bytes(), - snapshot, + &snapshot, ); cx.update_editor(|editor, _, cx| { diff --git a/crates/debugger_ui/src/session/running/loaded_source_list.rs b/crates/debugger_ui/src/session/running/loaded_source_list.rs index 921ebd8b5f..dd5487e042 100644 --- a/crates/debugger_ui/src/session/running/loaded_source_list.rs +++ b/crates/debugger_ui/src/session/running/loaded_source_list.rs @@ -13,8 +13,22 @@ pub(crate) struct LoadedSourceList { impl LoadedSourceList { pub fn new(session: Entity, cx: &mut Context) -> Self { + let weak_entity = cx.weak_entity(); let focus_handle = cx.focus_handle(); - let list = ListState::new(0, gpui::ListAlignment::Top, px(1000.)); + + let list = ListState::new( + 0, + gpui::ListAlignment::Top, + px(1000.), + move |ix, _window, cx| { + weak_entity + .upgrade() + .map(|loaded_sources| { + loaded_sources.update(cx, |this, cx| this.render_entry(ix, cx)) + }) + .unwrap_or(div().into_any()) + }, + ); let _subscription = cx.subscribe(&session, |this, _, event, cx| match event { SessionEvent::Stopped(_) | SessionEvent::LoadedSources => { @@ -57,7 +71,7 @@ impl LoadedSourceList { h_flex() .text_ui_xs(cx) .text_color(cx.theme().colors().text_muted) - .when_some(source.path, |this, path| this.child(path)), + .when_some(source.path.clone(), |this, path| this.child(path)), ) .into_any() } @@ -84,12 +98,6 @@ impl Render for LoadedSourceList { .track_focus(&self.focus_handle) .size_full() .p_1() - .child( - list( - self.list.clone(), - cx.processor(|this, ix, _window, cx| this.render_entry(ix, cx)), - ) - .size_full(), - ) + .child(list(self.list.clone()).size_full()) } } diff --git a/crates/debugger_ui/src/session/running/memory_view.rs b/crates/debugger_ui/src/session/running/memory_view.rs index e7b7963d3f..7b62a1d55d 100644 --- a/crates/debugger_ui/src/session/running/memory_view.rs +++ b/crates/debugger_ui/src/session/running/memory_view.rs @@ -18,9 +18,12 @@ use project::debugger::{MemoryCell, dap_command::DataBreakpointContext, session: use settings::Settings; use theme::ThemeSettings; use ui::{ - ContextMenu, Divider, DropdownMenu, FluentBuilder, IntoElement, PopoverMenuHandle, Render, - Scrollbar, ScrollbarState, StatefulInteractiveElement, Tooltip, prelude::*, + ActiveTheme, AnyElement, App, Color, Context, ContextMenu, Div, Divider, DropdownMenu, Element, + FluentBuilder, Icon, IconName, InteractiveElement, IntoElement, Label, LabelCommon, + ParentElement, Pixels, PopoverMenuHandle, Render, Scrollbar, ScrollbarState, SharedString, + StatefulInteractiveElement, Styled, TextSize, Tooltip, Window, div, h_flex, px, v_flex, }; +use util::ResultExt; use workspace::Workspace; use crate::{ToggleDataBreakpoint, session::running::stack_frame_list::StackFrameList}; @@ -31,7 +34,9 @@ pub(crate) struct MemoryView { workspace: WeakEntity, scroll_handle: UniformListScrollHandle, scroll_state: ScrollbarState, + show_scrollbar: bool, stack_frame_list: WeakEntity, + hide_scrollbar_task: Option>, focus_handle: FocusHandle, view_state: ViewState, query_editor: Entity, @@ -145,6 +150,8 @@ impl MemoryView { scroll_state, scroll_handle, stack_frame_list, + show_scrollbar: false, + hide_scrollbar_task: None, focus_handle: cx.focus_handle(), view_state, query_editor, @@ -161,42 +168,61 @@ impl MemoryView { .detach(); this } + fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context) { + const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1); + self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| { + cx.background_executor() + .timer(SCROLLBAR_SHOW_INTERVAL) + .await; + panel + .update(cx, |panel, cx| { + panel.show_scrollbar = false; + cx.notify(); + }) + .log_err(); + })) + } - fn render_vertical_scrollbar(&self, cx: &mut Context) -> Stateful
{ - div() - .occlude() - .id("memory-view-vertical-scrollbar") - .on_drag_move(cx.listener(|this, evt, _, cx| { - let did_handle = this.handle_scroll_drag(evt); - cx.notify(); - if did_handle { - cx.stop_propagation() - } - })) - .on_drag(ScrollbarDragging, |_, _, _, cx| cx.new(|_| Empty)) - .on_hover(|_, _, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|_, _, _, cx| { + fn render_vertical_scrollbar(&self, cx: &mut Context) -> Option> { + if !(self.show_scrollbar || self.scroll_state.is_dragging()) { + return None; + } + Some( + div() + .occlude() + .id("memory-view-vertical-scrollbar") + .on_drag_move(cx.listener(|this, evt, _, cx| { + let did_handle = this.handle_scroll_drag(evt); + cx.notify(); + if did_handle { + cx.stop_propagation() + } + })) + .on_drag(ScrollbarDragging, |_, _, _, cx| cx.new(|_| Empty)) + .on_hover(|_, _, cx| { cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _, cx| { - cx.notify(); - })) - .h_full() - .absolute() - .right_1() - .top_1() - .bottom_0() - .w(px(12.)) - .cursor_default() - .children(Scrollbar::vertical(self.scroll_state.clone()).map(|s| s.auto_hide(cx))) + }) + .on_any_mouse_down(|_, _, cx| { + cx.stop_propagation(); + }) + .on_mouse_up( + MouseButton::Left, + cx.listener(|_, _, _, cx| { + cx.stop_propagation(); + }), + ) + .on_scroll_wheel(cx.listener(|_, _, _, cx| { + cx.notify(); + })) + .h_full() + .absolute() + .right_1() + .top_1() + .bottom_0() + .w(px(12.)) + .cursor_default() + .children(Scrollbar::vertical(self.scroll_state.clone())), + ) } fn render_memory(&self, cx: &mut Context) -> UniformList { @@ -262,7 +288,7 @@ impl MemoryView { cx: &mut Context, ) { use parse_int::parse; - let Ok(as_address) = parse::(memory_reference) else { + let Ok(as_address) = parse::(&memory_reference) else { return; }; let access_size = evaluate_name @@ -461,7 +487,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 else { + let Some(data_id) = info.data_id.clone() else { return; }; _ = this.update(cx, |this, cx| { @@ -894,6 +920,15 @@ impl Render for MemoryView { .on_action(cx.listener(Self::page_up)) .size_full() .track_focus(&self.focus_handle) + .on_hover(cx.listener(|this, hovered, window, cx| { + if *hovered { + this.show_scrollbar = true; + this.hide_scrollbar_task.take(); + cx.notify(); + } else if !this.focus_handle.contains_focused(window, cx) { + this.hide_scrollbar(window, cx); + } + })) .child( h_flex() .w_full() @@ -931,7 +966,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, _)| { @@ -943,7 +978,7 @@ impl Render for MemoryView { ) .with_priority(1) })) - .child(self.render_vertical_scrollbar(cx)), + .children(self.render_vertical_scrollbar(cx)), ) } } diff --git a/crates/debugger_ui/src/session/running/module_list.rs b/crates/debugger_ui/src/session/running/module_list.rs index 7743cfbdee..74a9fb457a 100644 --- a/crates/debugger_ui/src/session/running/module_list.rs +++ b/crates/debugger_ui/src/session/running/module_list.rs @@ -157,7 +157,7 @@ impl ModuleList { h_flex() .text_ui_xs(cx) .text_color(cx.theme().colors().text_muted) - .when_some(module.path, |this, path| this.child(path)), + .when_some(module.path.clone(), |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) { let ix = match self.selected_ix { - _ if self.entries.is_empty() => None, + _ if self.entries.len() == 0 => None, None => Some(0), Some(ix) => { if ix == self.entries.len() - 1 { @@ -243,7 +243,7 @@ impl ModuleList { cx: &mut Context, ) { let ix = match self.selected_ix { - _ if self.entries.is_empty() => None, + _ if self.entries.len() == 0 => None, None => Some(self.entries.len() - 1), Some(ix) => { if ix == 0 { @@ -262,7 +262,7 @@ impl ModuleList { _window: &mut Window, cx: &mut Context, ) { - let ix = if !self.entries.is_empty() { + let ix = if self.entries.len() > 0 { Some(0) } else { None @@ -271,7 +271,7 @@ impl ModuleList { } fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context) { - let ix = if !self.entries.is_empty() { + let ix = if self.entries.len() > 0 { Some(self.entries.len() - 1) } else { None diff --git a/crates/debugger_ui/src/session/running/stack_frame_list.rs b/crates/debugger_ui/src/session/running/stack_frame_list.rs index a4ea4ab654..da3674c8e2 100644 --- a/crates/debugger_ui/src/session/running/stack_frame_list.rs +++ b/crates/debugger_ui/src/session/running/stack_frame_list.rs @@ -70,7 +70,13 @@ impl StackFrameList { _ => {} }); - let list_state = ListState::new(0, gpui::ListAlignment::Top, px(1000.)); + let list_state = ListState::new(0, gpui::ListAlignment::Top, px(1000.), { + let this = cx.weak_entity(); + move |ix, _window, cx| { + this.update(cx, |this, cx| this.render_entry(ix, cx)) + .unwrap_or(div().into_any()) + } + }); let scrollbar_state = ScrollbarState::new(list_state.clone()); let mut this = Self { @@ -126,7 +132,7 @@ impl StackFrameList { self.stack_frames(cx) .unwrap_or_default() .into_iter() - .map(|stack_frame| stack_frame.dap) + .map(|stack_frame| stack_frame.dap.clone()) .collect() } @@ -224,7 +230,7 @@ impl StackFrameList { let collapsed_entries = std::mem::take(&mut collapsed_entries); if !collapsed_entries.is_empty() { - entries.push(StackFrameEntry::Collapsed(collapsed_entries)); + entries.push(StackFrameEntry::Collapsed(collapsed_entries.clone())); } self.entries = entries; @@ -418,7 +424,7 @@ impl StackFrameList { let source = stack_frame.source.clone(); let is_selected_frame = Some(ix) == self.selected_ix; - let path = source.and_then(|s| s.path.or(s.name)); + let path = source.clone().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) @@ -493,7 +499,7 @@ impl StackFrameList { .child( IconButton::new( ("restart-stack-frame", stack_frame.id), - IconName::RotateCcw, + IconName::DebugRestart, ) .icon_size(IconSize::Small) .on_click(cx.listener({ @@ -621,7 +627,7 @@ impl StackFrameList { fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context) { let ix = match self.selected_ix { - _ if self.entries.is_empty() => None, + _ if self.entries.len() == 0 => None, None => Some(0), Some(ix) => { if ix == self.entries.len() - 1 { @@ -641,7 +647,7 @@ impl StackFrameList { cx: &mut Context, ) { let ix = match self.selected_ix { - _ if self.entries.is_empty() => None, + _ if self.entries.len() == 0 => None, None => Some(self.entries.len() - 1), Some(ix) => { if ix == 0 { @@ -660,7 +666,7 @@ impl StackFrameList { _window: &mut Window, cx: &mut Context, ) { - let ix = if !self.entries.is_empty() { + let ix = if self.entries.len() > 0 { Some(0) } else { None @@ -669,7 +675,7 @@ impl StackFrameList { } fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context) { - let ix = if !self.entries.is_empty() { + let ix = if self.entries.len() > 0 { Some(self.entries.len() - 1) } else { None @@ -702,14 +708,11 @@ impl StackFrameList { self.activate_selected_entry(window, cx); } - fn render_list(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - div().p_1().size_full().child( - list( - self.list_state.clone(), - cx.processor(|this, ix, _window, cx| this.render_entry(ix, cx)), - ) - .size_full(), - ) + fn render_list(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div() + .p_1() + .size_full() + .child(list(self.list_state.clone()).size_full()) } } diff --git a/crates/debugger_ui/src/session/running/variable_list.rs b/crates/debugger_ui/src/session/running/variable_list.rs index b396f0921e..906e482687 100644 --- a/crates/debugger_ui/src/session/running/variable_list.rs +++ b/crates/debugger_ui/src/session/running/variable_list.rs @@ -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).to_vec() + session.scopes(stack_frame_id, cx).iter().cloned().collect() }); let mut contains_local_scope = false; @@ -291,7 +291,7 @@ impl VariableList { } self.session.update(cx, |session, cx| { - !session.variables(scope.variables_reference, cx).is_empty() + session.variables(scope.variables_reference, cx).len() > 0 }) }) .map(|scope| { @@ -313,7 +313,7 @@ impl VariableList { watcher.variables_reference, watcher.variables_reference, EntryPath::for_watcher(watcher.expression.clone()), - DapEntry::Watcher(watcher), + DapEntry::Watcher(watcher.clone()), ) }) .collect::>(), @@ -947,7 +947,7 @@ impl VariableList { #[track_caller] #[cfg(test)] pub(crate) fn assert_visual_entries(&self, expected: Vec<&str>) { - const INDENT: &str = " "; + const INDENT: &'static 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.is_empty() { + if scopes.len() > 0 { idx += 1; } @@ -1107,7 +1107,7 @@ impl VariableList { let variable_value = value.clone(); this.on_click(cx.listener( move |this, click: &ClickEvent, window, cx| { - if click.click_count() < 2 { + if click.down.click_count < 2 { return; } let editor = Self::create_variable_editor( @@ -1289,7 +1289,7 @@ impl VariableList { }), ) .child(self.render_variable_value( - entry, + &entry, &variable_color, watcher.value.to_string(), cx, @@ -1301,6 +1301,8 @@ 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()); @@ -1468,6 +1470,7 @@ 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()); @@ -1491,7 +1494,7 @@ impl VariableList { }), ) .child(self.render_variable_value( - variable, + &variable, &variable_color, dap.value.clone(), cx, diff --git a/crates/debugger_ui/src/tests/attach_modal.rs b/crates/debugger_ui/src/tests/attach_modal.rs index 80e2b73d5a..906a7a0d4b 100644 --- a/crates/debugger_ui/src/tests/attach_modal.rs +++ b/crates/debugger_ui/src/tests/attach_modal.rs @@ -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()); }) diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index ab6d5cb960..6180831ea9 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -1330,6 +1330,7 @@ async fn test_unsetting_breakpoints_on_clear_breakpoint_action( let called_set_breakpoints = Arc::new(AtomicBool::new(false)); client.on_request::({ + let called_set_breakpoints = called_set_breakpoints.clone(); move |_, args| { assert!( args.breakpoints.is_none_or(|bps| bps.is_empty()), @@ -1444,6 +1445,7 @@ 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| { @@ -1781,8 +1783,9 @@ 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::(move |_, _| { - disconnect_clone.store(true, Ordering::SeqCst); + disconnect_clone_for_handler.store(true, Ordering::SeqCst); Ok(()) }); diff --git a/crates/debugger_ui/src/tests/new_process_modal.rs b/crates/debugger_ui/src/tests/new_process_modal.rs index bfc445cf67..0805060bf4 100644 --- a/crates/debugger_ui/src/tests/new_process_modal.rs +++ b/crates/debugger_ui/src/tests/new_process_modal.rs @@ -106,7 +106,9 @@ 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")) + input_path + .replace("$ZED_WORKTREE_ROOT", &path!("/test/worktree/path")) + .to_owned() } else { input_path.to_string() }; @@ -296,7 +298,7 @@ async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppConte let adapter_names = cx.update(|cx| { let registry = DapRegistry::global(cx); - registry.enumerate_adapters::>() + registry.enumerate_adapters() }); let zed_config = ZedDebugConfig { diff --git a/crates/debugger_ui/src/tests/variable_list.rs b/crates/debugger_ui/src/tests/variable_list.rs index 4cfdae093f..fbbd529641 100644 --- a/crates/debugger_ui/src/tests/variable_list.rs +++ b/crates/debugger_ui/src/tests/variable_list.rs @@ -1445,8 +1445,11 @@ 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, _, _| item.running_state().clone()); + let running_state = active_debug_session_panel(workspace, cx).update_in(cx, |item, _, _| { + let state = item.running_state().clone(); + + state + }); client .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { diff --git a/crates/diagnostics/Cargo.toml b/crates/diagnostics/Cargo.toml index fd678078e8..53b5792e10 100644 --- a/crates/diagnostics/Cargo.toml +++ b/crates/diagnostics/Cargo.toml @@ -18,6 +18,7 @@ collections.workspace = true component.workspace = true ctor.workspace = true editor.workspace = true +futures.workspace = true gpui.workspace = true indoc.workspace = true language.workspace = true diff --git a/crates/diagnostics/src/diagnostic_renderer.rs b/crates/diagnostics/src/diagnostic_renderer.rs index e9731f84ce..ce7b253702 100644 --- a/crates/diagnostics/src/diagnostic_renderer.rs +++ b/crates/diagnostics/src/diagnostic_renderer.rs @@ -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(' '); @@ -287,13 +287,15 @@ 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) + } }; } @@ -304,7 +306,7 @@ impl DiagnosticBlock { cx: &mut Context, ) { 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| { diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 1c27e820a0..ba64ba0eed 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -13,6 +13,7 @@ use editor::{ DEFAULT_MULTIBUFFER_CONTEXT, Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey, display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId}, }; +use futures::future::join_all; use gpui::{ AnyElement, AnyView, App, AsyncApp, Context, Entity, EventEmitter, FocusHandle, Focusable, Global, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, @@ -23,6 +24,7 @@ use language::{ }; use project::{ DiagnosticSummary, Project, ProjectPath, + lsp_store::rust_analyzer_ext::{cancel_flycheck, run_flycheck}, project_settings::{DiagnosticSeverity, ProjectSettings}, }; use settings::Settings; @@ -77,10 +79,17 @@ pub(crate) struct ProjectDiagnosticsEditor { paths_to_update: BTreeSet, include_warnings: bool, update_excerpts_task: Option>>, + cargo_diagnostics_fetch: CargoDiagnosticsFetchState, diagnostic_summary_update: Task<()>, _subscription: Subscription, } +struct CargoDiagnosticsFetchState { + fetch_task: Option>, + cancel_task: Option>, + diagnostic_sources: Arc>, +} + impl EventEmitter for ProjectDiagnosticsEditor {} const DIAGNOSTICS_UPDATE_DELAY: Duration = Duration::from_millis(50); @@ -168,9 +177,9 @@ impl ProjectDiagnosticsEditor { } project::Event::DiagnosticsUpdated { language_server_id, - paths, + path, } => { - this.paths_to_update.extend(paths.clone()); + this.paths_to_update.insert(path.clone()); let project = project.clone(); this.diagnostic_summary_update = cx.spawn(async move |this, cx| { cx.background_executor() @@ -184,9 +193,9 @@ impl ProjectDiagnosticsEditor { cx.emit(EditorEvent::TitleChanged); if this.editor.focus_handle(cx).contains_focused(window, cx) || this.focus_handle.contains_focused(window, cx) { - log::debug!("diagnostics updated for server {language_server_id}, paths {paths:?}. recording change"); + log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. recording change"); } else { - log::debug!("diagnostics updated for server {language_server_id}, paths {paths:?}. updating excerpts"); + log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. updating excerpts"); this.update_stale_excerpts(window, cx); } } @@ -251,7 +260,11 @@ impl ProjectDiagnosticsEditor { ) }); this.diagnostics.clear(); - this.update_all_excerpts(window, cx); + this.update_all_diagnostics(false, window, cx); + }) + .detach(); + cx.observe_release(&cx.entity(), |editor, _, cx| { + editor.stop_cargo_diagnostics_fetch(cx); }) .detach(); @@ -268,10 +281,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_excerpts(window, cx); + this.update_all_diagnostics(true, window, cx); this } @@ -355,10 +373,22 @@ impl ProjectDiagnosticsEditor { window: &mut Window, cx: &mut Context, ) { - if self.update_excerpts_task.is_some() { - self.update_excerpts_task = None; + 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); + } } else { - self.update_all_excerpts(window, cx); + if self.update_excerpts_task.is_some() { + self.update_excerpts_task = None; + } else { + self.update_all_diagnostics(false, window, cx); + } } cx.notify(); } @@ -376,6 +406,73 @@ impl ProjectDiagnosticsEditor { } } + fn update_all_diagnostics( + &mut self, + first_launch: bool, + window: &mut Window, + cx: &mut Context, + ) { + 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>, + cx: &mut Context, + ) { + 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) { @@ -431,7 +528,7 @@ impl ProjectDiagnosticsEditor { lsp::DiagnosticSeverity::ERROR }; - cx.spawn_in(window, async move |this, cx| { + cx.spawn_in(window, async move |this, mut cx| { let diagnostics = buffer_snapshot .diagnostics_in_range::<_, text::Anchor>( Point::zero()..buffer_snapshot.max_point(), @@ -445,7 +542,7 @@ impl ProjectDiagnosticsEditor { return true; } this.diagnostics.insert(buffer_id, diagnostics.clone()); - false + return false; })?; if unchanged { return Ok(()); @@ -498,7 +595,7 @@ impl ProjectDiagnosticsEditor { b.initial_range.clone(), DEFAULT_MULTIBUFFER_CONTEXT, buffer_snapshot.clone(), - cx, + &mut cx, ) .await; let i = excerpt_ranges @@ -542,15 +639,17 @@ impl ProjectDiagnosticsEditor { #[cfg(test)] let cloned_blocks = blocks.clone(); - 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); + 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); + } } } @@ -600,6 +699,30 @@ impl ProjectDiagnosticsEditor { }) }) } + + pub fn cargo_diagnostics_sources(&self, cx: &App) -> Vec { + 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() + } } impl Focusable for ProjectDiagnosticsEditor { @@ -857,16 +980,18 @@ 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()) - && let Some(end_row) = (outline_range.start.row..outline_range.end.row + 1) + { + if 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, + )); + } } } } diff --git a/crates/diagnostics/src/diagnostics_tests.rs b/crates/diagnostics/src/diagnostics_tests.rs index 4a544f9ea7..1364aaf853 100644 --- a/crates/diagnostics/src/diagnostics_tests.rs +++ b/crates/diagnostics/src/diagnostics_tests.rs @@ -862,7 +862,7 @@ 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.is_empty() { + if snapshot.buffer_snapshot.len() > 0 { let position = rng.gen_range(0..snapshot.buffer_snapshot.len()); let position = snapshot.buffer_snapshot.clip_offset(position, Bias::Left); log::info!( @@ -873,10 +873,10 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S editor.splice_inlays( &[], - vec![Inlay::edit_prediction( + vec![Inlay::inline_completion( post_inc(&mut next_inlay_id), snapshot.buffer_snapshot.anchor_before(position), - Rope::from_iter(["Test inlay ", "next_inlay_id"]), + format!("Test inlay {next_inlay_id}"), )], cx, ); @@ -971,7 +971,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().unwrap().read(cx).lsp_store()); + cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store()); cx.set_state(indoc! {" ˇfn func(abc def: i32) -> u32 { @@ -1065,7 +1065,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().unwrap().read(cx).lsp_store()); + cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store()); cx.set_state(indoc! {" ˇfn func(abc def: i32) -> u32 { @@ -1239,7 +1239,7 @@ async fn test_diagnostics_with_links(cx: &mut TestAppContext) { } "}); let lsp_store = - cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store()); + cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store()); cx.update(|_, cx| { lsp_store.update(cx, |lsp_store, cx| { @@ -1293,7 +1293,7 @@ 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().unwrap().read(cx).lsp_store()); + cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store()); cx.update(|_, cx| { lsp_store.update(cx, |lsp_store, cx| { lsp_store.update_diagnostics( @@ -1450,7 +1450,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().unwrap().read(cx).lsp_store()); + cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store()); cx.set_state(indoc! {"error warning info hiˇnt"}); diff --git a/crates/diagnostics/src/toolbar_controls.rs b/crates/diagnostics/src/toolbar_controls.rs index 404db39164..9a7dcbe62f 100644 --- a/crates/diagnostics/src/toolbar_controls.rs +++ b/crates/diagnostics/src/toolbar_controls.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use crate::{ProjectDiagnosticsEditor, ToggleDiagnosticsRefresh}; use gpui::{Context, Entity, EventEmitter, ParentElement, Render, WeakEntity, Window}; use ui::prelude::*; @@ -13,18 +15,26 @@ impl Render for ToolbarControls { let mut include_warnings = false; let mut has_stale_excerpts = 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 = diagnostics.update_excerpts_task.is_some() - || diagnostics - .project - .read(cx) - .language_servers_running_disk_based_diagnostics(cx) - .next() - .is_some(); + 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() + }; } let tooltip = if include_warnings { @@ -44,7 +54,7 @@ impl Render for ToolbarControls { .map(|div| { if is_updating { div.child( - IconButton::new("stop-updating", IconName::Stop) + IconButton::new("stop-updating", IconName::StopFilled) .icon_color(Color::Info) .shape(IconButtonShape::Square) .tooltip(Tooltip::for_action_title( @@ -54,6 +64,7 @@ impl Render for ToolbarControls { .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; cx.notify(); }); @@ -62,10 +73,10 @@ impl Render for ToolbarControls { ) } else { div.child( - IconButton::new("refresh-diagnostics", IconName::ArrowCircle) + IconButton::new("refresh-diagnostics", IconName::Update) .icon_color(Color::Info) .shape(IconButtonShape::Square) - .disabled(!has_stale_excerpts) + .disabled(!has_stale_excerpts && !fetch_cargo_diagnostics) .tooltip(Tooltip::for_action_title( "Refresh diagnostics", &ToggleDiagnosticsRefresh, @@ -73,8 +84,17 @@ impl Render for ToolbarControls { .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| { - diagnostics.update_all_excerpts(window, cx); + if fetch_cargo_diagnostics { + diagnostics.fetch_cargo_diagnostics( + cargo_diagnostics_sources, + cx, + ); + } else { + diagnostics.update_all_excerpts(window, cx); + } }); } } diff --git a/crates/docs_preprocessor/src/main.rs b/crates/docs_preprocessor/src/main.rs index c8c3dc54b7..1448f4cb52 100644 --- a/crates/docs_preprocessor/src/main.rs +++ b/crates/docs_preprocessor/src/main.rs @@ -8,7 +8,7 @@ use std::borrow::Cow; use std::collections::{HashMap, HashSet}; use std::io::{self, Read}; use std::process; -use std::sync::{LazyLock, OnceLock}; +use std::sync::LazyLock; use util::paths::PathExt; static KEYMAP_MACOS: LazyLock = LazyLock::new(|| { @@ -19,13 +19,9 @@ static KEYMAP_LINUX: LazyLock = LazyLock::new(|| { load_keymap("keymaps/default-linux.json").expect("Failed to load Linux keymap") }); -static KEYMAP_WINDOWS: LazyLock = LazyLock::new(|| { - load_keymap("keymaps/default-windows.json").expect("Failed to load Windows keymap") -}); - static ALL_ACTIONS: LazyLock> = LazyLock::new(dump_all_gpui_actions); -const FRONT_MATTER_COMMENT: &str = ""; +const FRONT_MATTER_COMMENT: &'static str = ""; fn main() -> Result<()> { zlog::init(); @@ -65,13 +61,15 @@ impl PreprocessorError { for alias in action.deprecated_aliases { if alias == &action_name { return PreprocessorError::DeprecatedActionUsed { - used: action_name, + used: action_name.clone(), should_be: action.name.to_string(), }; } } } - PreprocessorError::ActionNotFound { action_name } + PreprocessorError::ActionNotFound { + action_name: action_name.to_string(), + } } } @@ -103,13 +101,12 @@ fn handle_preprocessing() -> Result<()> { let mut errors = HashSet::::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: &str = "\x1b[31m"; - const ANSI_RESET: &str = "\x1b[0m"; + const ANSI_RED: &'static str = "\x1b[31m"; + const ANSI_RESET: &'static str = "\x1b[0m"; for error in &errors { eprintln!("{ANSI_RED}ERROR{ANSI_RESET}: {}", error); } @@ -132,7 +129,7 @@ fn handle_frontmatter(book: &mut Book, errors: &mut HashSet) let Some((name, value)) = line.split_once(':') else { errors.insert(PreprocessorError::InvalidFrontmatterLine(format!( "{}: {}", - chapter_breadcrumbs(chapter), + chapter_breadcrumbs(&chapter), line ))); continue; @@ -146,20 +143,11 @@ fn handle_frontmatter(book: &mut Book, errors: &mut HashSet) &serde_json::to_string(&metadata).expect("Failed to serialize metadata"), ) }); - 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(), - ); + match new_content { + Cow::Owned(content) => { + chapter.content = content; + } + Cow::Borrowed(_) => {} } }); } @@ -220,7 +208,6 @@ fn find_binding(os: &str, action: &str) -> Option { let keymap = match os { "macos" => &KEYMAP_MACOS, "linux" | "freebsd" => &KEYMAP_LINUX, - "windows" => &KEYMAP_WINDOWS, _ => unreachable!("Not a valid OS: {}", os), }; @@ -295,7 +282,6 @@ struct ActionDef { name: &'static str, human_name: String, deprecated_aliases: &'static [&'static str], - docs: Option<&'static str>, } fn dump_all_gpui_actions() -> Vec { @@ -304,13 +290,12 @@ fn dump_all_gpui_actions() -> Vec { name: action.name, human_name: command_palette::humanize_action_name(action.name), deprecated_aliases: action.deprecated_aliases, - docs: action.documentation, }) .collect::>(); actions.sort_by_key(|a| a.name); - actions + return actions; } fn handle_postprocessing() -> Result<()> { @@ -403,7 +388,7 @@ fn handle_postprocessing() -> Result<()> { let meta_title = format!("{} | {}", page_title, meta_title); zlog::trace!(logger => "Updating {:?}", pretty_path(&file, &root_dir)); let contents = contents.replace("#description#", meta_description); - let contents = title_regex() + let contents = TITLE_REGEX .replace(&contents, |_: ®ex::Captures| { format!("{}", meta_title) }) @@ -417,75 +402,21 @@ 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) } + const TITLE_REGEX: std::cell::LazyCell = + std::cell::LazyCell::new(|| Regex::new(r"\s*(.*?)\s*").unwrap()); fn extract_title_from_page(contents: &str, pretty_path: &std::path::Path) -> String { - let title_tag_contents = &title_regex() - .captures(contents) + let title_tag_contents = &TITLE_REGEX + .captures(&contents) .with_context(|| format!("Failed to find title in {:?}", pretty_path)) .expect("Page has element")[1]; - - title_tag_contents + let title = title_tag_contents .trim() .strip_suffix("- Zed") .unwrap_or(title_tag_contents) .trim() - .to_string() + .to_string(); + title } } - -fn title_regex() -> &'static Regex { - static TITLE_REGEX: OnceLock<Regex> = OnceLock::new(); - TITLE_REGEX.get_or_init(|| Regex::new(r"<title>\s*(.*?)\s*").unwrap()) -} - -fn generate_big_table_of_actions() -> String { - let actions = &*ALL_ACTIONS; - let mut output = String::new(); - - let mut actions_sorted = actions.iter().collect::>(); - actions_sorted.sort_by_key(|a| a.name); - - // Start the definition list with custom styling for better spacing - output.push_str("
\n"); - - for action in actions_sorted.into_iter() { - // Add the humanized action name as the term with margin - output.push_str( - "
", - ); - output.push_str(&action.human_name); - output.push_str("
\n"); - - // Add the definition with keymap name and description - output.push_str("
\n"); - - // Add the description, escaping HTML if needed - if let Some(description) = action.docs { - output.push_str( - &description - .replace("&", "&") - .replace("<", "<") - .replace(">", ">"), - ); - output.push_str("
\n"); - } - output.push_str("Keymap Name: "); - output.push_str(action.name); - output.push_str("
\n"); - if !action.deprecated_aliases.is_empty() { - output.push_str("Deprecated Aliases:"); - for alias in action.deprecated_aliases.iter() { - output.push_str(""); - output.push_str(alias); - output.push_str(", "); - } - } - output.push_str("\n
\n"); - } - - // Close the definition list - output.push_str("
\n"); - - output -} diff --git a/crates/edit_prediction/LICENSE-GPL b/crates/edit_prediction/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/edit_prediction/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/edit_prediction_button/LICENSE-GPL b/crates/edit_prediction_button/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/edit_prediction_button/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 339f98ae8b..ab2d1c8ecb 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -48,7 +48,7 @@ fs.workspace = true git.workspace = true gpui.workspace = true indoc.workspace = true -edit_prediction.workspace = true +inline_completion.workspace = true itertools.workspace = true language.workspace = true linkify.workspace = true diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index ce02c4d2bf..1212651cb3 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -273,16 +273,6 @@ 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)] @@ -325,8 +315,9 @@ actions!( [ /// Accepts the full edit prediction. AcceptEditPrediction, + /// Accepts a partial Copilot suggestion. + AcceptPartialCopilotSuggestion, /// Accepts a partial edit prediction. - #[action(deprecated_aliases = ["editor::AcceptPartialCopilotSuggestion"])] AcceptPartialEditPrediction, /// Adds a cursor above the current selection. AddSelectionAbove, @@ -682,6 +673,8 @@ 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. @@ -753,6 +746,5 @@ actions!( UniqueLinesCaseInsensitive, /// Removes duplicate lines (case-sensitive). UniqueLinesCaseSensitive, - UnwrapSyntaxNode ] ); diff --git a/crates/editor/src/clangd_ext.rs b/crates/editor/src/clangd_ext.rs index c78d4c83c0..b745bf8c37 100644 --- a/crates/editor/src/clangd_ext.rs +++ b/crates/editor/src/clangd_ext.rs @@ -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 { - language.name() == "C++".into() || language.name() == "C".into() + return language.name() == "C++".into() || language.name() == "C".into(); } pub fn switch_source_header( @@ -29,14 +29,16 @@ pub fn switch_source_header( return; }; - let Some((_, _, server_to_query, buffer)) = - find_specific_language_server_in_selection(editor, cx, is_c_language, CLANGD_SERVER_NAME) - else { - return; - }; + let server_lookup = + find_specific_language_server_in_selection(editor, cx, is_c_language, CLANGD_SERVER_NAME); let project = project.clone(); let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client(); cx.spawn_in(window, async move |_editor, cx| { + let Some((_, _, server_to_query, buffer)) = + server_lookup.await + else { + return Ok(()); + }; let source_file = buffer.read_with(cx, |buffer, _| { buffer.file().map(|file| file.path()).map(|path| path.to_string_lossy().to_string()).unwrap_or_else(|| "Unknown".to_string()) })?; @@ -104,6 +106,6 @@ pub fn apply_related_actions(editor: &Entity, 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); } } diff --git a/crates/editor/src/code_completion_tests.rs b/crates/editor/src/code_completion_tests.rs index a1d9f04a9c..fd8db29584 100644 --- a/crates/editor/src/code_completion_tests.rs +++ b/crates/editor/src/code_completion_tests.rs @@ -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) } diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index 96809d6877..4ae2a14ca7 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -321,7 +321,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() @@ -514,7 +514,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, + entry_range.clone(), RESOLVE_BEFORE_ITEMS, RESOLVE_AFTER_ITEMS, entries.len(), @@ -1111,8 +1111,10 @@ 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()) - == Some(query_char) + word.chars() + .next() + .and_then(|c| c.to_lowercase().next()) + .map_or(false, |word_char| word_char == query_char) }) }) .unwrap_or(false); diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index c16e4a6ddb..5425d5a8b9 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -635,7 +635,7 @@ pub(crate) struct Highlights<'a> { } #[derive(Clone, Copy, Debug)] -pub struct EditPredictionStyles { +pub struct InlineCompletionStyles { pub insertion: HighlightStyle, pub whitespace: HighlightStyle, } @@ -643,7 +643,7 @@ pub struct EditPredictionStyles { #[derive(Default, Debug, Clone, Copy)] pub struct HighlightStyles { pub inlay_hint: Option, - pub edit_prediction: Option, + pub inline_completion: Option, } #[derive(Clone)] @@ -958,7 +958,7 @@ impl DisplaySnapshot { language_aware, HighlightStyles { inlay_hint: Some(editor_style.inlay_hints_style), - edit_prediction: Some(editor_style.edit_prediction_styles), + inline_completion: Some(editor_style.inline_completion_styles), }, ) .flat_map(|chunk| { @@ -969,13 +969,13 @@ impl DisplaySnapshot { 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 - && 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); + 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); + } } } @@ -991,7 +991,7 @@ impl DisplaySnapshot { if let Some(severity) = chunk.diagnostic_severity.filter(|severity| { self.diagnostics_max_severity .into_lsp() - .is_some_and(|max_severity| severity <= &max_severity) + .map_or(false, |max_severity| severity <= &max_severity) }) { if chunk.is_unnecessary { diagnostic_highlight.fade_out = Some(editor_style.unnecessary_code_fade); @@ -2036,7 +2036,7 @@ pub mod tests { map.update(cx, |map, cx| { map.splice_inlays( &[], - vec![Inlay::edit_prediction( + vec![Inlay::inline_completion( 0, buffer_snapshot.anchor_after(0), "\n", @@ -2351,12 +2351,11 @@ 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() - && *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() { + if *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)); @@ -2902,12 +2901,11 @@ 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() - && 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() { + if 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)); } diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index b073fe7be7..85495a2611 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -22,7 +22,7 @@ use std::{ atomic::{AtomicUsize, Ordering::SeqCst}, }, }; -use sum_tree::{Bias, Dimensions, SumTree, Summary, TreeMap}; +use sum_tree::{Bias, SumTree, Summary, TreeMap}; use text::{BufferId, Edit}; use ui::ElementId; @@ -290,10 +290,7 @@ pub enum Block { ExcerptBoundary { excerpt: ExcerptInfo, height: u32, - }, - BufferHeader { - excerpt: ExcerptInfo, - height: u32, + starts_new_buffer: bool, }, } @@ -306,37 +303,27 @@ 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 { .. } - | Block::BufferHeader { .. } => true, + Block::ExcerptBoundary { .. } | Block::FoldedBuffer { .. } => true, } } pub fn height(&self) -> u32 { match self { Block::Custom(block) => block.height.unwrap_or(0), - Block::ExcerptBoundary { height, .. } - | Block::FoldedBuffer { height, .. } - | Block::BufferHeader { height, .. } => *height, + Block::ExcerptBoundary { height, .. } | Block::FoldedBuffer { height, .. } => *height, } } pub fn style(&self) -> BlockStyle { match self { Block::Custom(block) => block.style, - Block::ExcerptBoundary { .. } - | Block::FoldedBuffer { .. } - | Block::BufferHeader { .. } => BlockStyle::Sticky, + Block::ExcerptBoundary { .. } | Block::FoldedBuffer { .. } => BlockStyle::Sticky, } } @@ -345,7 +332,6 @@ impl Block { Block::Custom(block) => matches!(block.placement, BlockPlacement::Above(_)), Block::FoldedBuffer { .. } => false, Block::ExcerptBoundary { .. } => true, - Block::BufferHeader { .. } => true, } } @@ -354,7 +340,6 @@ impl Block { Block::Custom(block) => matches!(block.placement, BlockPlacement::Near(_)), Block::FoldedBuffer { .. } => false, Block::ExcerptBoundary { .. } => false, - Block::BufferHeader { .. } => false, } } @@ -366,7 +351,6 @@ impl Block { ), Block::FoldedBuffer { .. } => false, Block::ExcerptBoundary { .. } => false, - Block::BufferHeader { .. } => false, } } @@ -375,7 +359,6 @@ impl Block { Block::Custom(block) => matches!(block.placement, BlockPlacement::Replace(_)), Block::FoldedBuffer { .. } => true, Block::ExcerptBoundary { .. } => false, - Block::BufferHeader { .. } => false, } } @@ -384,7 +367,6 @@ impl Block { Block::Custom(_) => false, Block::FoldedBuffer { .. } => true, Block::ExcerptBoundary { .. } => true, - Block::BufferHeader { .. } => true, } } @@ -392,8 +374,9 @@ impl Block { match self { Block::Custom(_) => false, Block::FoldedBuffer { .. } => true, - Block::ExcerptBoundary { .. } => false, - Block::BufferHeader { .. } => true, + Block::ExcerptBoundary { + starts_new_buffer, .. + } => *starts_new_buffer, } } } @@ -410,14 +393,14 @@ impl Debug for Block { .field("first_excerpt", &first_excerpt) .field("height", height) .finish(), - Self::ExcerptBoundary { excerpt, height } => f + Self::ExcerptBoundary { + starts_new_buffer, + excerpt, + height, + } => f .debug_struct("ExcerptBoundary") .field("excerpt", excerpt) - .field("height", height) - .finish(), - Self::BufferHeader { excerpt, height } => f - .debug_struct("BufferHeader") - .field("excerpt", excerpt) + .field("starts_new_buffer", starts_new_buffer) .field("height", height) .finish(), } @@ -433,7 +416,7 @@ struct TransformSummary { } pub struct BlockChunks<'a> { - transforms: sum_tree::Cursor<'a, Transform, Dimensions>, + transforms: sum_tree::Cursor<'a, Transform, (BlockRow, WrapRow)>, input_chunks: wrap_map::WrapChunks<'a>, input_chunk: Chunk<'a>, output_row: u32, @@ -443,7 +426,7 @@ pub struct BlockChunks<'a> { #[derive(Clone)] pub struct BlockRows<'a> { - transforms: sum_tree::Cursor<'a, Transform, Dimensions>, + transforms: sum_tree::Cursor<'a, Transform, (BlockRow, WrapRow)>, input_rows: wrap_map::WrapRows<'a>, output_row: BlockRow, started: bool, @@ -542,22 +525,26 @@ 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() - && 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(); + 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(); - // 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; + // 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; + } } } } @@ -620,7 +607,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().is_some_and(|b| b.place_below()) { + if transform.block.as_ref().map_or(false, |b| b.place_below()) { cursor.next(); } else { break; @@ -670,20 +657,22 @@ impl BlockMap { .iter() .filter_map(|block| { let placement = block.placement.to_wrap_row(wrap_snapshot)?; - if let BlockPlacement::Above(row) = placement - && row < new_start - { - return None; + if let BlockPlacement::Above(row) = placement { + if row < new_start { + return None; + } } Some((placement, Block::Custom(block.clone()))) }), ); - blocks_in_edit.extend(self.header_and_footer_blocks( - buffer, - (start_bound, end_bound), - wrap_snapshot, - )); + if buffer.show_headers() { + blocks_in_edit.extend(self.header_and_footer_blocks( + buffer, + (start_bound, end_bound), + wrap_snapshot, + )); + } BlockMap::sort_blocks(&mut blocks_in_edit); @@ -786,7 +775,7 @@ impl BlockMap { if self.buffers_with_disabled_headers.contains(&new_buffer_id) { continue; } - if self.folded_buffers.contains(&new_buffer_id) && buffer.show_headers() { + if self.folded_buffers.contains(&new_buffer_id) { let mut last_excerpt_end_row = first_excerpt.end_row; while let Some(next_boundary) = boundaries.peek() { @@ -819,24 +808,20 @@ impl BlockMap { } } - let starts_new_buffer = new_buffer_id.is_some(); - let block = if starts_new_buffer && buffer.show_headers() { + if new_buffer_id.is_some() { height += self.buffer_header_height; - Block::BufferHeader { - excerpt: excerpt_boundary.next, - height, - } - } else if excerpt_boundary.prev.is_some() { + } else { height += self.excerpt_header_height; + } + + return Some(( + BlockPlacement::Above(WrapRow(wrap_row)), Block::ExcerptBoundary { excerpt: excerpt_boundary.next, height, - } - } else { - continue; - }; - - return Some((BlockPlacement::Above(WrapRow(wrap_row)), block)); + starts_new_buffer: new_buffer_id.is_some(), + }, + )); } }) } @@ -861,25 +846,13 @@ impl BlockMap { ( 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::BufferHeader { .. }, - Block::Custom(_), - ) => Ordering::Less, - ( - Block::Custom(_), - Block::ExcerptBoundary { .. } | Block::BufferHeader { .. }, - ) => Ordering::Greater, + (Block::ExcerptBoundary { .. }, Block::Custom(_)) => Ordering::Less, + (Block::Custom(_), Block::ExcerptBoundary { .. }) => Ordering::Greater, (Block::Custom(block_a), Block::Custom(block_b)) => block_a .priority .cmp(&block_b.priority) @@ -997,17 +970,17 @@ impl BlockMapReader<'_> { .unwrap_or(self.wrap_snapshot.max_point().row() + 1), ); - let mut cursor = self.transforms.cursor::>(&()); + let mut cursor = self.transforms.cursor::<(WrapRow, BlockRow)>(&()); cursor.seek(&start_wrap_row, Bias::Left); while let Some(transform) = cursor.item() { if cursor.start().0 > end_wrap_row { break; } - if let Some(BlockId::Custom(id)) = transform.block.as_ref().map(|block| block.id()) - && id == block_id - { - return Some(cursor.start().1); + if let Some(BlockId::Custom(id)) = transform.block.as_ref().map(|block| block.id()) { + if id == block_id { + return Some(cursor.start().1); + } } cursor.next(); } @@ -1319,21 +1292,21 @@ impl BlockSnapshot { ) -> BlockChunks<'a> { let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows); - let mut cursor = self.transforms.cursor::>(&()); + let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); cursor.seek(&BlockRow(rows.start), Bias::Right); let transform_output_start = cursor.start().0.0; let transform_input_start = cursor.start().1.0; let mut input_start = transform_input_start; let mut input_end = transform_input_start; - 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, - ); + 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, + ); + } } BlockChunks { @@ -1351,12 +1324,12 @@ impl BlockSnapshot { } pub(super) fn row_infos(&self, start_row: BlockRow) -> BlockRows<'_> { - let mut cursor = self.transforms.cursor::>(&()); + let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); cursor.seek(&start_row, Bias::Right); - let Dimensions(output_start, input_start, _) = cursor.start(); + let (output_start, input_start) = cursor.start(); let overshoot = if cursor .item() - .is_some_and(|transform| transform.block.is_none()) + .map_or(false, |transform| transform.block.is_none()) { start_row.0 - output_start.0 } else { @@ -1386,7 +1359,7 @@ impl BlockSnapshot { && transform .block .as_ref() - .is_some_and(|block| block.height() > 0)) + .map_or(false, |block| block.height() > 0)) { break; } @@ -1408,9 +1381,7 @@ impl BlockSnapshot { while let Some(transform) = cursor.item() { match &transform.block { - Some( - Block::ExcerptBoundary { excerpt, .. } | Block::BufferHeader { excerpt, .. }, - ) => { + Some(Block::ExcerptBoundary { excerpt, .. }) => { return Some(StickyHeaderExcerpt { excerpt }); } Some(block) if block.is_buffer_header() => return None, @@ -1470,14 +1441,14 @@ impl BlockSnapshot { } pub fn longest_row_in_range(&self, range: Range) -> BlockRow { - let mut cursor = self.transforms.cursor::>(&()); + let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); cursor.seek(&range.start, Bias::Right); let mut longest_row = range.start; let mut longest_row_chars = 0; if let Some(transform) = cursor.item() { if transform.block.is_none() { - let Dimensions(output_start, input_start, _) = cursor.start(); + let (output_start, input_start) = cursor.start(); let overshoot = range.start.0 - output_start.0; let wrap_start_row = input_start.0 + overshoot; let wrap_end_row = cmp::min( @@ -1501,18 +1472,18 @@ impl BlockSnapshot { longest_row_chars = summary.longest_row_chars; } - 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); + if let Some(transform) = cursor.item() { + if transform.block.is_none() { + let (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); + } } } } @@ -1521,10 +1492,10 @@ impl BlockSnapshot { } pub(super) fn line_len(&self, row: BlockRow) -> u32 { - let mut cursor = self.transforms.cursor::>(&()); + let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); cursor.seek(&BlockRow(row.0), Bias::Right); if let Some(transform) = cursor.item() { - let Dimensions(output_start, input_start, _) = cursor.start(); + let (output_start, input_start) = cursor.start(); let overshoot = row.0 - output_start.0; if transform.block.is_some() { 0 @@ -1539,13 +1510,13 @@ impl BlockSnapshot { } pub(super) fn is_block_line(&self, row: BlockRow) -> bool { - let mut cursor = self.transforms.cursor::>(&()); + let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); cursor.seek(&row, Bias::Right); - cursor.item().is_some_and(|t| t.block.is_some()) + cursor.item().map_or(false, |t| t.block.is_some()) } pub(super) fn is_folded_buffer_header(&self, row: BlockRow) -> bool { - let mut cursor = self.transforms.cursor::>(&()); + let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); cursor.seek(&row, Bias::Right); let Some(transform) = cursor.item() else { return false; @@ -1557,18 +1528,18 @@ impl BlockSnapshot { let wrap_point = self .wrap_snapshot .make_wrap_point(Point::new(row.0, 0), Bias::Left); - let mut cursor = self.transforms.cursor::>(&()); + let mut cursor = self.transforms.cursor::<(WrapRow, BlockRow)>(&()); cursor.seek(&WrapRow(wrap_point.row()), Bias::Right); - cursor.item().is_some_and(|transform| { + cursor.item().map_or(false, |transform| { transform .block .as_ref() - .is_some_and(|block| block.is_replacement()) + .map_or(false, |block| block.is_replacement()) }) } pub fn clip_point(&self, point: BlockPoint, bias: Bias) -> BlockPoint { - let mut cursor = self.transforms.cursor::>(&()); + let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); cursor.seek(&BlockRow(point.row), Bias::Right); let max_input_row = WrapRow(self.transforms.summary().input_rows); @@ -1578,19 +1549,20 @@ impl BlockSnapshot { loop { if let Some(transform) = cursor.item() { - let Dimensions(output_start_row, input_start_row, _) = cursor.start(); - let Dimensions(output_end_row, input_end_row, _) = cursor.end(); + let (output_start_row, input_start_row) = cursor.start(); + let (output_end_row, input_end_row) = cursor.end(); let output_start = Point::new(output_start_row.0, 0); let input_start = Point::new(input_start_row.0, 0); let input_end = Point::new(input_end_row.0, 0); match transform.block.as_ref() { Some(block) => { - if block.is_replacement() - && (((bias == Bias::Left || search_left) && output_start <= point.0) - || (!search_left && output_start >= point.0)) - { - return BlockPoint(output_start); + if block.is_replacement() { + if ((bias == Bias::Left || search_left) && output_start <= point.0) + || (!search_left && output_start >= point.0) + { + return BlockPoint(output_start); + } } } None => { @@ -1627,13 +1599,13 @@ impl BlockSnapshot { } pub fn to_block_point(&self, wrap_point: WrapPoint) -> BlockPoint { - let mut cursor = self.transforms.cursor::>(&()); + let mut cursor = self.transforms.cursor::<(WrapRow, BlockRow)>(&()); cursor.seek(&WrapRow(wrap_point.row()), Bias::Right); if let Some(transform) = cursor.item() { if transform.block.is_some() { BlockPoint::new(cursor.start().1.0, 0) } else { - let Dimensions(input_start_row, output_start_row, _) = cursor.start(); + let (input_start_row, output_start_row) = cursor.start(); let input_start = Point::new(input_start_row.0, 0); let output_start = Point::new(output_start_row.0, 0); let input_overshoot = wrap_point.0 - input_start; @@ -1645,7 +1617,7 @@ impl BlockSnapshot { } pub fn to_wrap_point(&self, block_point: BlockPoint, bias: Bias) -> WrapPoint { - let mut cursor = self.transforms.cursor::>(&()); + let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); cursor.seek(&BlockRow(block_point.row), Bias::Right); if let Some(transform) = cursor.item() { match transform.block.as_ref() { @@ -1683,7 +1655,7 @@ impl BlockChunks<'_> { if transform .block .as_ref() - .is_some_and(|block| block.height() == 0) + .map_or(false, |block| block.height() == 0) { self.transforms.next(); } else { @@ -1694,7 +1666,7 @@ impl BlockChunks<'_> { if self .transforms .item() - .is_some_and(|transform| transform.block.is_none()) + .map_or(false, |transform| transform.block.is_none()) { let start_input_row = self.transforms.start().1.0; let start_output_row = self.transforms.start().0.0; @@ -1804,7 +1776,7 @@ impl Iterator for BlockRows<'_> { if transform .block .as_ref() - .is_some_and(|block| block.height() == 0) + .map_or(false, |block| block.height() == 0) { self.transforms.next(); } else { @@ -1816,7 +1788,7 @@ impl Iterator for BlockRows<'_> { if transform .block .as_ref() - .is_none_or(|block| block.is_replacement()) + .map_or(true, |block| block.is_replacement()) { self.input_rows.seek(self.transforms.start().1.0); } @@ -2189,7 +2161,7 @@ mod tests { } let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx); - let (_, inlay_snapshot) = InlayMap::new(multi_buffer_snapshot); + let (_, inlay_snapshot) = InlayMap::new(multi_buffer_snapshot.clone()); 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); @@ -2308,7 +2280,7 @@ mod tests { new_heights.insert(block_ids[0], 3); block_map_writer.resize(new_heights); - let snapshot = block_map.read(wraps_snapshot, Default::default()); + let snapshot = block_map.read(wraps_snapshot.clone(), 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"); } @@ -2318,6 +2290,8 @@ mod tests { fn test_blocks_on_wrapped_lines(cx: &mut gpui::TestAppContext) { cx.update(init_test); + let _font_id = cx.text_system().font_id(&font("Helvetica")).unwrap(); + let text = "one two three\nfour five six\nseven eight"; let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx)); @@ -2393,14 +2367,16 @@ 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, buffer_subscription.consume().into_inner()); + let (inlay_snapshot, inlay_edits) = inlay_map.sync( + buffer_snapshot.clone(), + 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, wrap_edits); + let blocks_snapshot = block_map.read(wraps_snapshot.clone(), wrap_edits); assert_eq!(blocks_snapshot.text(), "line1\n\n\n\n\nline5"); let buffer_snapshot = buffer.update(cx, |buffer, cx| { @@ -2485,7 +2461,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, Default::default()); + let blocks_snapshot = block_map.read(wraps_snapshot.clone(), 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" @@ -2824,7 +2800,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, Patch::default()); + let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default()); let blocks = blocks_snapshot .blocks_in_range(0..u32::MAX) .collect::>(); @@ -2877,7 +2853,7 @@ mod tests { assert_eq!(buffer_ids.len(), 1); let buffer_id = buffer_ids[0]; - let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot); + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); let (_, wrap_snapshot) = @@ -2891,7 +2867,7 @@ mod tests { buffer.read_with(cx, |buffer, cx| { writer.fold_buffers([buffer_id], buffer, cx); }); - let blocks_snapshot = block_map.read(wrap_snapshot, Patch::default()); + let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default()); let blocks = blocks_snapshot .blocks_in_range(0..u32::MAX) .collect::>(); @@ -2899,7 +2875,12 @@ mod tests { 1, blocks .iter() - .filter(|(_, block)| { matches!(block, Block::FoldedBuffer { .. }) }) + .filter(|(_, block)| { + match block { + Block::FoldedBuffer { .. } => true, + _ => false, + } + }) .count(), "Should have one folded block, producing a header of the second buffer" ); @@ -3216,9 +3197,9 @@ mod tests { // so we special case row 0 to assume a leading '\n'. // // Linehood is the birthright of strings. - let input_text_lines = input_text.split('\n').enumerate().peekable(); + let mut input_text_lines = input_text.split('\n').enumerate().peekable(); let mut block_row = 0; - for (wrap_row, input_line) in input_text_lines { + while let Some((wrap_row, input_line)) = input_text_lines.next() { let wrap_row = wrap_row as u32; let multibuffer_row = wraps_snapshot .to_point(WrapPoint::new(wrap_row, 0), Bias::Left) @@ -3249,32 +3230,34 @@ 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 { - is_in_replace_block = true; + if wrap_row >= replace_range.start().0 { + 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'); + 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(); + + sorted_blocks_iter.next(); } - expected_text.push_str(&text); - - for _ in 1..block.height() { - expected_buffer_rows.push(None); - } - block_row += block.height(); - - sorted_blocks_iter.next(); } } @@ -3558,7 +3541,7 @@ mod tests { ..buffer_snapshot.anchor_after(Point::new(1, 0))], false, ); - let blocks_snapshot = block_map.read(wraps_snapshot, Default::default()); + let blocks_snapshot = block_map.read(wraps_snapshot.clone(), Default::default()); assert_eq!(blocks_snapshot.text(), "abc\n\ndef\nghi\njkl\nmno"); } diff --git a/crates/editor/src/display_map/custom_highlights.rs b/crates/editor/src/display_map/custom_highlights.rs index f3737ea4b7..ae69e9cf8c 100644 --- a/crates/editor/src/display_map/custom_highlights.rs +++ b/crates/editor/src/display_map/custom_highlights.rs @@ -77,7 +77,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,18 +88,18 @@ 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; } highlight_endpoints.push(HighlightEndpoint { - offset: range.start.to_offset(buffer), + offset: range.start.to_offset(&buffer), is_start: true, tag, style, }); highlight_endpoints.push(HighlightEndpoint { - offset: range.end.to_offset(buffer), + offset: range.end.to_offset(&buffer), is_start: false, tag, style, diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 42f46fb749..829d34ff58 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -17,7 +17,7 @@ use std::{ sync::Arc, usize, }; -use sum_tree::{Bias, Cursor, Dimensions, FilterCursor, SumTree, Summary, TreeMap}; +use sum_tree::{Bias, Cursor, FilterCursor, SumTree, Summary, TreeMap}; use ui::IntoElement as _; use util::post_inc; @@ -98,9 +98,7 @@ impl FoldPoint { } pub fn to_inlay_point(self, snapshot: &FoldSnapshot) -> InlayPoint { - let mut cursor = snapshot - .transforms - .cursor::>(&()); + let mut cursor = snapshot.transforms.cursor::<(FoldPoint, InlayPoint)>(&()); cursor.seek(&self, Bias::Right); let overshoot = self.0 - cursor.start().0.0; InlayPoint(cursor.start().1.0 + overshoot) @@ -109,7 +107,7 @@ impl FoldPoint { pub fn to_offset(self, snapshot: &FoldSnapshot) -> FoldOffset { let mut cursor = snapshot .transforms - .cursor::>(&()); + .cursor::<(FoldPoint, TransformSummary)>(&()); cursor.seek(&self, Bias::Right); let overshoot = self.0 - cursor.start().1.output.lines; let mut offset = cursor.start().1.output.len; @@ -289,25 +287,25 @@ impl FoldMapWriter<'_> { let ChunkRendererId::Fold(id) = id else { continue; }; - 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(), - }); + 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(), + }); - 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), + }, + ); + } } } @@ -417,18 +415,18 @@ impl FoldMap { cursor.seek(&InlayOffset(0), Bias::Right); while let Some(mut edit) = inlay_edits_iter.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(); - } - }, - &(), - ); + 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(); + } + }, + &(), + ); + } } new_transforms.append(cursor.slice(&edit.old.start, Bias::Left), &()); edit.new.start -= edit.old.start - *cursor.start(); @@ -491,14 +489,14 @@ impl FoldMap { while folds .peek() - .is_some_and(|(_, fold_range)| fold_range.start < edit.new.end) + .map_or(false, |(_, 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().is_some_and(|(next_fold, next_fold_range)| { + while folds.peek().map_or(false, |(next_fold, next_fold_range)| { next_fold_range.start < fold_range.end || (next_fold_range.start == fold_range.end && fold.placeholder.merge_adjacent @@ -569,20 +567,19 @@ impl FoldMap { let mut old_transforms = self .snapshot .transforms - .cursor::>(&()); - let mut new_transforms = - new_transforms.cursor::>(&()); + .cursor::<(InlayOffset, FoldOffset)>(&()); + let mut new_transforms = new_transforms.cursor::<(InlayOffset, FoldOffset)>(&()); for mut edit in inlay_edits { old_transforms.seek(&edit.old.start, Bias::Left); - if old_transforms.item().is_some_and(|t| t.is_fold()) { + if old_transforms.item().map_or(false, |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().is_some_and(|t| t.is_fold()) { + if old_transforms.item().map_or(false, |t| t.is_fold()) { old_transforms.next(); edit.old.end = old_transforms.start().0; } @@ -590,14 +587,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().is_some_and(|t| t.is_fold()) { + if new_transforms.item().map_or(false, |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().is_some_and(|t| t.is_fold()) { + if new_transforms.item().map_or(false, |t| t.is_fold()) { new_transforms.next(); edit.new.end = new_transforms.start().0; } @@ -654,9 +651,7 @@ impl FoldSnapshot { pub fn text_summary_for_range(&self, range: Range) -> TextSummary { let mut summary = TextSummary::default(); - let mut cursor = self - .transforms - .cursor::>(&()); + let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>(&()); cursor.seek(&range.start, Bias::Right); if let Some(transform) = cursor.item() { let start_in_transform = range.start.0 - cursor.start().0.0; @@ -705,11 +700,9 @@ impl FoldSnapshot { } pub fn to_fold_point(&self, point: InlayPoint, bias: Bias) -> FoldPoint { - let mut cursor = self - .transforms - .cursor::>(&()); + let mut cursor = self.transforms.cursor::<(InlayPoint, FoldPoint)>(&()); cursor.seek(&point, Bias::Right); - if cursor.item().is_some_and(|t| t.is_fold()) { + if cursor.item().map_or(false, |t| t.is_fold()) { if bias == Bias::Left || point == cursor.start().0 { cursor.start().1 } else { @@ -741,9 +734,7 @@ impl FoldSnapshot { } let fold_point = FoldPoint::new(start_row, 0); - let mut cursor = self - .transforms - .cursor::>(&()); + let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>(&()); cursor.seek(&fold_point, Bias::Left); let overshoot = fold_point.0 - cursor.start().0.0; @@ -788,7 +779,7 @@ impl FoldSnapshot { let inlay_offset = self.inlay_snapshot.to_inlay_offset(buffer_offset); let mut cursor = self.transforms.cursor::(&()); cursor.seek(&inlay_offset, Bias::Right); - cursor.item().is_some_and(|t| t.placeholder.is_some()) + cursor.item().map_or(false, |t| t.placeholder.is_some()) } pub fn is_line_folded(&self, buffer_row: MultiBufferRow) -> bool { @@ -825,9 +816,7 @@ impl FoldSnapshot { language_aware: bool, highlights: Highlights<'a>, ) -> FoldChunks<'a> { - let mut transform_cursor = self - .transforms - .cursor::>(&()); + let mut transform_cursor = self.transforms.cursor::<(FoldOffset, InlayOffset)>(&()); transform_cursor.seek(&range.start, Bias::Right); let inlay_start = { @@ -839,7 +828,7 @@ impl FoldSnapshot { let inlay_end = if transform_cursor .item() - .is_none_or(|transform| transform.is_fold()) + .map_or(true, |transform| transform.is_fold()) { inlay_start } else if range.end < transform_end.0 { @@ -882,9 +871,7 @@ impl FoldSnapshot { } pub fn clip_point(&self, point: FoldPoint, bias: Bias) -> FoldPoint { - let mut cursor = self - .transforms - .cursor::>(&()); + let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>(&()); cursor.seek(&point, Bias::Right); if let Some(transform) = cursor.item() { let transform_start = cursor.start().0.0; @@ -1209,7 +1196,7 @@ impl<'a> sum_tree::Dimension<'a, FoldSummary> for usize { #[derive(Clone)] pub struct FoldRows<'a> { - cursor: Cursor<'a, Transform, Dimensions>, + cursor: Cursor<'a, Transform, (FoldPoint, InlayPoint)>, input_rows: InlayBufferRows<'a>, fold_point: FoldPoint, } @@ -1326,7 +1313,7 @@ impl DerefMut for ChunkRendererContext<'_, '_> { } pub struct FoldChunks<'a> { - transform_cursor: Cursor<'a, Transform, Dimensions>, + transform_cursor: Cursor<'a, Transform, (FoldOffset, InlayOffset)>, inlay_chunks: InlayChunks<'a>, inlay_chunk: Option<(InlayOffset, InlayChunk<'a>)>, inlay_offset: InlayOffset, @@ -1348,7 +1335,7 @@ impl FoldChunks<'_> { let inlay_end = if self .transform_cursor .item() - .is_none_or(|transform| transform.is_fold()) + .map_or(true, |transform| transform.is_fold()) { inlay_start } else if range.end < transform_end.0 { @@ -1461,9 +1448,9 @@ impl FoldOffset { pub fn to_point(self, snapshot: &FoldSnapshot) -> FoldPoint { let mut cursor = snapshot .transforms - .cursor::>(&()); + .cursor::<(FoldOffset, TransformSummary)>(&()); cursor.seek(&self, Bias::Right); - let overshoot = if cursor.item().is_none_or(|t| t.is_fold()) { + let overshoot = if cursor.item().map_or(true, |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; @@ -1475,9 +1462,7 @@ impl FoldOffset { #[cfg(test)] pub fn to_inlay_offset(self, snapshot: &FoldSnapshot) -> InlayOffset { - let mut cursor = snapshot - .transforms - .cursor::>(&()); + let mut cursor = snapshot.transforms.cursor::<(FoldOffset, InlayOffset)>(&()); cursor.seek(&self, Bias::Right); let overshoot = self.0 - cursor.start().0.0; InlayOffset(cursor.start().1.0 + overshoot) @@ -1557,7 +1542,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); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); let mut map = FoldMap::new(inlay_snapshot.clone()).0; let (mut writer, _, _) = map.write(inlay_snapshot, vec![]); @@ -1636,7 +1621,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); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); { let mut map = FoldMap::new(inlay_snapshot.clone()).0; @@ -1712,7 +1697,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); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); let mut map = FoldMap::new(inlay_snapshot.clone()).0; let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); @@ -1720,7 +1705,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, vec![]); + let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]); assert_eq!(snapshot.text(), "aa⋯cccc\nd⋯eeeee"); let buffer_snapshot = buffer.update(cx, |buffer, cx| { @@ -1747,7 +1732,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, vec![]); + let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]); let fold_ranges = snapshot .folds_in_range(Point::new(1, 0)..Point::new(1, 3)) .map(|fold| { @@ -1782,7 +1767,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, vec![]); + let (mut initial_snapshot, _) = map.read(inlay_snapshot.clone(), vec![]); let mut snapshot_edits = Vec::new(); let mut next_inlay_id = 0; diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 3db9d10fdc..a36d18ff6d 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -10,7 +10,7 @@ use std::{ ops::{Add, AddAssign, Range, Sub, SubAssign}, sync::Arc, }; -use sum_tree::{Bias, Cursor, Dimensions, SumTree}; +use sum_tree::{Bias, Cursor, SumTree}; use text::{Patch, Rope}; use ui::{ActiveTheme, IntoElement as _, ParentElement as _, Styled as _, div}; @@ -48,16 +48,16 @@ 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.reversed_chars_at(text.len()).next() != Some(' ') { - text.push(" "); + if hint.padding_right && !text.ends_with(' ') { + text.push(' '); } - if hint.padding_left && text.chars_at(0).next() != Some(' ') { - text.push_front(" "); + if hint.padding_left && !text.starts_with(' ') { + text.insert(0, ' '); } Self { id: InlayId::Hint(id), position, - text, + text: text.into(), color: None, } } @@ -81,9 +81,9 @@ impl Inlay { } } - pub fn edit_prediction>(id: usize, position: Anchor, text: T) -> Self { + pub fn inline_completion>(id: usize, position: Anchor, text: T) -> Self { Self { - id: InlayId::EditPrediction(id), + id: InlayId::InlineCompletion(id), position, text: text.into(), color: None, @@ -235,14 +235,14 @@ impl<'a> sum_tree::Dimension<'a, TransformSummary> for Point { #[derive(Clone)] pub struct InlayBufferRows<'a> { - transforms: Cursor<'a, Transform, Dimensions>, + transforms: Cursor<'a, Transform, (InlayPoint, Point)>, buffer_rows: MultiBufferRows<'a>, inlay_row: u32, max_buffer_row: MultiBufferRow, } pub struct InlayChunks<'a> { - transforms: Cursor<'a, Transform, Dimensions>, + transforms: Cursor<'a, Transform, (InlayOffset, usize)>, buffer_chunks: CustomHighlightsChunks<'a>, buffer_chunk: Option>, inlay_chunks: Option>, @@ -340,13 +340,15 @@ impl<'a> Iterator for InlayChunks<'a> { let mut renderer = None; let mut highlight_style = match inlay.id { - InlayId::EditPrediction(_) => self.highlight_styles.edit_prediction.map(|s| { - if inlay.text.chars().all(|c| c.is_whitespace()) { - s.whitespace - } else { - s.insertion - } - }), + InlayId::InlineCompletion(_) => { + self.highlight_styles.inline_completion.map(|s| { + if inlay.text.chars().all(|c| c.is_whitespace()) { + s.whitespace + } else { + s.insertion + } + }) + } InlayId::Hint(_) => self.highlight_styles.inlay_hint, InlayId::DebuggerValue(_) => self.highlight_styles.inlay_hint, InlayId::Color(_) => { @@ -551,17 +553,15 @@ impl InlayMap { } else { let mut inlay_edits = Patch::default(); let mut new_transforms = SumTree::default(); - let mut cursor = snapshot - .transforms - .cursor::>(&()); + let mut cursor = snapshot.transforms.cursor::<(usize, InlayOffset)>(&()); 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() - && cursor.end().0 == buffer_edit.old.start - { - push_isomorphic(&mut new_transforms, *transform); - cursor.next(); + if let Some(Transform::Isomorphic(transform)) = cursor.item() { + if 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 +625,7 @@ impl InlayMap { // we can push its remainder. if buffer_edits_iter .peek() - .is_none_or(|edit| edit.old.start >= cursor.end().0) + .map_or(true, |edit| edit.old.start >= cursor.end().0) { let transform_start = new_transforms.summary().input.len; let transform_end = @@ -737,13 +737,13 @@ impl InlayMap { Inlay::mock_hint( post_inc(next_inlay_id), snapshot.buffer.anchor_at(position, bias), - &text, + text.clone(), ) } else { - Inlay::edit_prediction( + Inlay::inline_completion( post_inc(next_inlay_id), snapshot.buffer.anchor_at(position, bias), - &text, + text.clone(), ) }; let inlay_id = next_inlay.id; @@ -772,20 +772,20 @@ impl InlaySnapshot { pub fn to_point(&self, offset: InlayOffset) -> InlayPoint { let mut cursor = self .transforms - .cursor::>(&()); + .cursor::<(InlayOffset, (InlayPoint, usize))>(&()); cursor.seek(&offset, Bias::Right); let overshoot = offset.0 - cursor.start().0.0; match cursor.item() { Some(Transform::Isomorphic(_)) => { - let buffer_offset_start = cursor.start().2; + let buffer_offset_start = cursor.start().1.1; let buffer_offset_end = buffer_offset_start + overshoot; let buffer_start = self.buffer.offset_to_point(buffer_offset_start); let buffer_end = self.buffer.offset_to_point(buffer_offset_end); - InlayPoint(cursor.start().1.0 + (buffer_end - buffer_start)) + InlayPoint(cursor.start().1.0.0 + (buffer_end - buffer_start)) } Some(Transform::Inlay(inlay)) => { let overshoot = inlay.text.offset_to_point(overshoot); - InlayPoint(cursor.start().1.0 + overshoot) + InlayPoint(cursor.start().1.0.0 + overshoot) } None => self.max_point(), } @@ -802,26 +802,26 @@ impl InlaySnapshot { pub fn to_offset(&self, point: InlayPoint) -> InlayOffset { let mut cursor = self .transforms - .cursor::>(&()); + .cursor::<(InlayPoint, (InlayOffset, Point))>(&()); cursor.seek(&point, Bias::Right); let overshoot = point.0 - cursor.start().0.0; match cursor.item() { Some(Transform::Isomorphic(_)) => { - let buffer_point_start = cursor.start().2; + let buffer_point_start = cursor.start().1.1; let buffer_point_end = buffer_point_start + overshoot; let buffer_offset_start = self.buffer.point_to_offset(buffer_point_start); let buffer_offset_end = self.buffer.point_to_offset(buffer_point_end); - InlayOffset(cursor.start().1.0 + (buffer_offset_end - buffer_offset_start)) + InlayOffset(cursor.start().1.0.0 + (buffer_offset_end - buffer_offset_start)) } Some(Transform::Inlay(inlay)) => { let overshoot = inlay.text.point_to_offset(overshoot); - InlayOffset(cursor.start().1.0 + overshoot) + InlayOffset(cursor.start().1.0.0 + overshoot) } None => self.len(), } } pub fn to_buffer_point(&self, point: InlayPoint) -> Point { - let mut cursor = self.transforms.cursor::>(&()); + let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>(&()); cursor.seek(&point, Bias::Right); match cursor.item() { Some(Transform::Isomorphic(_)) => { @@ -833,9 +833,7 @@ impl InlaySnapshot { } } pub fn to_buffer_offset(&self, offset: InlayOffset) -> usize { - let mut cursor = self - .transforms - .cursor::>(&()); + let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(&()); cursor.seek(&offset, Bias::Right); match cursor.item() { Some(Transform::Isomorphic(_)) => { @@ -848,9 +846,7 @@ impl InlaySnapshot { } pub fn to_inlay_offset(&self, offset: usize) -> InlayOffset { - let mut cursor = self - .transforms - .cursor::>(&()); + let mut cursor = self.transforms.cursor::<(usize, InlayOffset)>(&()); cursor.seek(&offset, Bias::Left); loop { match cursor.item() { @@ -883,7 +879,7 @@ impl InlaySnapshot { } } pub fn to_inlay_point(&self, point: Point) -> InlayPoint { - let mut cursor = self.transforms.cursor::>(&()); + let mut cursor = self.transforms.cursor::<(Point, InlayPoint)>(&()); cursor.seek(&point, Bias::Left); loop { match cursor.item() { @@ -917,7 +913,7 @@ impl InlaySnapshot { } pub fn clip_point(&self, mut point: InlayPoint, mut bias: Bias) -> InlayPoint { - let mut cursor = self.transforms.cursor::>(&()); + let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>(&()); cursor.seek(&point, Bias::Left); loop { match cursor.item() { @@ -1014,9 +1010,7 @@ impl InlaySnapshot { pub fn text_summary_for_range(&self, range: Range) -> TextSummary { let mut summary = TextSummary::default(); - let mut cursor = self - .transforms - .cursor::>(&()); + let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(&()); cursor.seek(&range.start, Bias::Right); let overshoot = range.start.0 - cursor.start().0.0; @@ -1064,7 +1058,7 @@ impl InlaySnapshot { } pub fn row_infos(&self, row: u32) -> InlayBufferRows<'_> { - let mut cursor = self.transforms.cursor::>(&()); + let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>(&()); let inlay_point = InlayPoint::new(row, 0); cursor.seek(&inlay_point, Bias::Left); @@ -1106,9 +1100,7 @@ impl InlaySnapshot { language_aware: bool, highlights: Highlights<'a>, ) -> InlayChunks<'a> { - let mut cursor = self - .transforms - .cursor::>(&()); + let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(&()); cursor.seek(&range.start, Bias::Right); let buffer_range = self.to_buffer_offset(range.start)..self.to_buffer_offset(range.end); @@ -1305,29 +1297,6 @@ 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); @@ -1420,7 +1389,7 @@ mod tests { buffer.read(cx).snapshot(cx).anchor_before(3), "|123|", ), - Inlay::edit_prediction( + Inlay::inline_completion( post_inc(&mut next_inlay_id), buffer.read(cx).snapshot(cx).anchor_after(3), "|456|", @@ -1640,7 +1609,7 @@ mod tests { buffer.read(cx).snapshot(cx).anchor_before(4), "|456|", ), - Inlay::edit_prediction( + Inlay::inline_completion( post_inc(&mut next_inlay_id), buffer.read(cx).snapshot(cx).anchor_before(7), "\n|567|\n", @@ -1717,7 +1686,7 @@ mod tests { (offset, inlay.clone()) }) .collect::>(); - let mut expected_text = Rope::from(&buffer_snapshot.text()); + let mut expected_text = Rope::from(buffer_snapshot.text()); for (offset, inlay) in inlays.iter().rev() { expected_text.replace(*offset..*offset, &inlay.text.to_string()); } diff --git a/crates/editor/src/display_map/invisibles.rs b/crates/editor/src/display_map/invisibles.rs index 0712ddf9e2..199986f2a4 100644 --- a/crates/editor/src/display_map/invisibles.rs +++ b/crates/editor/src/display_map/invisibles.rs @@ -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: &[&str] = &[ +const C0_SYMBOLS: &'static [&'static str] = &[ "␀", "␁", "␂", "␃", "␄", "␅", "␆", "␇", "␈", "␉", "␊", "␋", "␌", "␍", "␎", "␏", "␐", "␑", "␒", "␓", "␔", "␕", "␖", "␗", "␘", "␙", "␚", "␛", "␜", "␝", "␞", "␟", ]; -const DEL: &str = "␡"; +const DEL: &'static str = "␡"; // generated using ucd-generate: ucd-generate general-category --include Format --chars ucd-16.0.0 -pub const FORMAT: &[(char, char)] = &[ +pub const FORMAT: &'static [(char, char)] = &[ ('\u{ad}', '\u{ad}'), ('\u{600}', '\u{605}'), ('\u{61c}', '\u{61c}'), @@ -93,7 +93,7 @@ pub const FORMAT: &[(char, char)] = &[ ]; // hand-made base on https://invisible-characters.com (Excluding Cf) -pub const OTHER: &[(char, char)] = &[ +pub const OTHER: &'static [(char, char)] = &[ ('\u{034f}', '\u{034f}'), ('\u{115F}', '\u{1160}'), ('\u{17b4}', '\u{17b5}'), @@ -107,7 +107,7 @@ pub const OTHER: &[(char, char)] = &[ ]; // a subset of FORMAT/OTHER that may appear within glyphs -const PRESERVE: &[(char, char)] = &[ +const PRESERVE: &'static [(char, char)] = &[ ('\u{034f}', '\u{034f}'), ('\u{200d}', '\u{200d}'), ('\u{17b4}', '\u{17b5}'), diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index 6f5df9bb8e..eb5d57d484 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -116,7 +116,7 @@ impl TabMap { state.new.end = edit.new.end; Some(None) // Skip this edit, it's merged } else { - let new_state = edit; + let new_state = edit.clone(); let result = Some(Some(state.clone())); // Yield the previous edit **state = new_state; result @@ -611,7 +611,7 @@ mod tests { fn test_expand_tabs(cx: &mut gpui::App) { let buffer = MultiBuffer::build_simple("", cx); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot); + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); @@ -628,7 +628,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); + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); @@ -675,7 +675,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); + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); @@ -689,7 +689,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); + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); @@ -749,7 +749,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); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); log::info!("InlayMap text: {:?}", inlay_snapshot.text()); let (mut fold_map, _) = FoldMap::new(inlay_snapshot.clone()); fold_map.randomly_mutate(&mut rng); @@ -758,7 +758,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, tab_size); + let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size); let tabs_snapshot = tab_map.set_max_expansion_column(32); let text = text::Rope::from(tabs_snapshot.text().as_str()); diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index 500ec3a0bb..d55577826e 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -9,7 +9,7 @@ use multi_buffer::{MultiBufferSnapshot, RowInfo}; use smol::future::yield_now; use std::sync::LazyLock; use std::{cmp, collections::VecDeque, mem, ops::Range, time::Duration}; -use sum_tree::{Bias, Cursor, Dimensions, SumTree}; +use sum_tree::{Bias, Cursor, SumTree}; use text::Patch; pub use super::tab_map::TextSummary; @@ -55,7 +55,7 @@ pub struct WrapChunks<'a> { input_chunk: Chunk<'a>, output_position: WrapPoint, max_output_row: u32, - transforms: Cursor<'a, Transform, Dimensions>, + transforms: Cursor<'a, Transform, (WrapPoint, TabPoint)>, snapshot: &'a WrapSnapshot, } @@ -66,7 +66,7 @@ pub struct WrapRows<'a> { output_row: u32, soft_wrapped: bool, max_output_row: u32, - transforms: Cursor<'a, Transform, Dimensions>, + transforms: Cursor<'a, Transform, (WrapPoint, TabPoint)>, } impl WrapRows<'_> { @@ -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().is_some_and(|t| t.is_isomorphic()) { + if self.transforms.item().map_or(false, |t| t.is_isomorphic()) { input_row += start_row - self.transforms.start().0.row(); } - self.soft_wrapped = self.transforms.item().is_some_and(|t| !t.is_isomorphic()); + self.soft_wrapped = self.transforms.item().map_or(false, |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 - && 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 { + 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) + }); - 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(); + })); + } } } } @@ -598,12 +598,10 @@ impl WrapSnapshot { ) -> WrapChunks<'a> { let output_start = WrapPoint::new(rows.start, 0); let output_end = WrapPoint::new(rows.end, 0); - let mut transforms = self - .transforms - .cursor::>(&()); + let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); transforms.seek(&output_start, Bias::Right); let mut input_start = TabPoint(transforms.start().1.0); - if transforms.item().is_some_and(|t| t.is_isomorphic()) { + if transforms.item().map_or(false, |t| t.is_isomorphic()) { input_start.0 += output_start.0 - transforms.start().0.0; } let input_end = self @@ -628,13 +626,11 @@ impl WrapSnapshot { } pub fn line_len(&self, row: u32) -> u32 { - let mut cursor = self - .transforms - .cursor::>(&()); + let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); cursor.seek(&WrapPoint::new(row + 1, 0), Bias::Left); if cursor .item() - .is_some_and(|transform| transform.is_isomorphic()) + .map_or(false, |transform| transform.is_isomorphic()) { let overshoot = row - cursor.start().0.row(); let tab_row = cursor.start().1.row() + overshoot; @@ -655,9 +651,7 @@ impl WrapSnapshot { let start = WrapPoint::new(rows.start, 0); let end = WrapPoint::new(rows.end, 0); - let mut cursor = self - .transforms - .cursor::>(&()); + let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); cursor.seek(&start, Bias::Right); if let Some(transform) = cursor.item() { let start_in_transform = start.0 - cursor.start().0.0; @@ -727,15 +721,13 @@ impl WrapSnapshot { } pub fn row_infos(&self, start_row: u32) -> WrapRows<'_> { - let mut transforms = self - .transforms - .cursor::>(&()); + let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); transforms.seek(&WrapPoint::new(start_row, 0), Bias::Left); let mut input_row = transforms.start().1.row(); - if transforms.item().is_some_and(|t| t.is_isomorphic()) { + if transforms.item().map_or(false, |t| t.is_isomorphic()) { input_row += start_row - transforms.start().0.row(); } - let soft_wrapped = transforms.item().is_some_and(|t| !t.is_isomorphic()); + let soft_wrapped = transforms.item().map_or(false, |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 { @@ -749,12 +741,10 @@ impl WrapSnapshot { } pub fn to_tab_point(&self, point: WrapPoint) -> TabPoint { - let mut cursor = self - .transforms - .cursor::>(&()); + let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); cursor.seek(&point, Bias::Right); let mut tab_point = cursor.start().1.0; - if cursor.item().is_some_and(|t| t.is_isomorphic()) { + if cursor.item().map_or(false, |t| t.is_isomorphic()) { tab_point += point.0 - cursor.start().0.0; } TabPoint(tab_point) @@ -769,9 +759,7 @@ impl WrapSnapshot { } pub fn tab_point_to_wrap_point(&self, point: TabPoint) -> WrapPoint { - let mut cursor = self - .transforms - .cursor::>(&()); + let mut cursor = self.transforms.cursor::<(TabPoint, WrapPoint)>(&()); cursor.seek(&point, Bias::Right); WrapPoint(cursor.start().1.0 + (point.0 - cursor.start().0.0)) } @@ -780,7 +768,7 @@ impl WrapSnapshot { if bias == Bias::Left { let mut cursor = self.transforms.cursor::(&()); cursor.seek(&point, Bias::Right); - if cursor.item().is_some_and(|t| !t.is_isomorphic()) { + if cursor.item().map_or(false, |t| !t.is_isomorphic()) { point = *cursor.start(); *point.column_mut() -= 1; } @@ -796,9 +784,7 @@ impl WrapSnapshot { *point.column_mut() = 0; - let mut cursor = self - .transforms - .cursor::>(&()); + let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); cursor.seek(&point, Bias::Right); if cursor.item().is_none() { cursor.prev(); @@ -818,9 +804,7 @@ impl WrapSnapshot { pub fn next_row_boundary(&self, mut point: WrapPoint) -> Option { point.0 += Point::new(1, 0); - let mut cursor = self - .transforms - .cursor::>(&()); + let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); cursor.seek(&point, Bias::Right); while let Some(transform) = cursor.item() { if transform.is_isomorphic() && cursor.start().1.column() == 0 { @@ -901,7 +885,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().is_some_and(|t| t.is_isomorphic()) { + if self.transforms.item().map_or(false, |t| t.is_isomorphic()) { input_start.0 += output_start.0 - self.transforms.start().0.0; } let input_end = self @@ -993,7 +977,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().is_some_and(|t| t.is_isomorphic()) { + if self.transforms.item().map_or(false, |t| t.is_isomorphic()) { self.input_buffer_row = self.input_buffer_rows.next().unwrap(); self.soft_wrapped = false; } else { @@ -1065,12 +1049,12 @@ impl sum_tree::Item for Transform { } fn push_isomorphic(transforms: &mut Vec, summary: TextSummary) { - if let Some(last_transform) = transforms.last_mut() - && last_transform.is_isomorphic() - { - last_transform.summary.input += &summary; - last_transform.summary.output += &summary; - return; + if let Some(last_transform) = transforms.last_mut() { + if last_transform.is_isomorphic() { + last_transform.summary.input += &summary; + last_transform.summary.output += &summary; + return; + } } transforms.push(Transform::isomorphic(summary)); } @@ -1223,7 +1207,7 @@ mod tests { let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap(); let font = test_font(); - let _font_id = text_system.resolve_font(&font); + let _font_id = text_system.font_id(&font); let font_size = px(14.0); log::info!("Tab size: {}", tab_size); @@ -1461,7 +1445,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)); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 80680ae9c0..3516eff45c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -43,65 +43,50 @@ pub mod tasks; #[cfg(test)] mod code_completion_tests; #[cfg(test)] -mod edit_prediction_tests; -#[cfg(test)] mod editor_tests; +#[cfg(test)] +mod inline_completion_tests; mod signature_help; #[cfg(any(test, feature = "test-support"))] pub mod test; pub(crate) use actions::*; -pub use display_map::{ChunkRenderer, ChunkRendererContext, DisplayPoint, FoldPlaceholder}; -pub use edit_prediction::Direction; -pub use editor_settings::{ - CurrentLineHighlight, DocumentColorsRenderMode, EditorSettings, HideMouseMode, - ScrollBeyondLastLine, ScrollbarAxes, SearchSettings, ShowMinimap, ShowScrollbar, -}; -pub use editor_settings_controls::*; -pub use element::{ - CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition, -}; -pub use git::blame::BlameRenderer; -pub use hover_popover::hover_markdown_style; -pub use items::MAX_TAB_TITLE_LEN; -pub use lsp::CompletionContext; -pub use lsp_ext::lsp_tasks; -pub use multi_buffer::{ - Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, PathKey, - RowInfo, ToOffset, ToPoint, -}; -pub use proposed_changes_editor::{ - ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar, -}; -pub use text::Bias; - -use ::git::{ - Restore, - blame::{BlameEntry, ParsedCommitMessage}, -}; +pub use actions::{AcceptEditPrediction, OpenExcerpts, OpenExcerptsSplit}; use aho_corasick::AhoCorasick; use anyhow::{Context as _, Result, anyhow}; use blink_manager::BlinkManager; use buffer_diff::DiffHunkStatus; -use client::{Collaborator, ParticipantIndex}; +use client::{Collaborator, DisableAiSettings, ParticipantIndex}; use clock::{AGENT_REPLICA_ID, ReplicaId}; -use code_context_menus::{ - AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu, - CompletionsMenu, ContextMenuOrigin, -}; use collections::{BTreeMap, HashMap, HashSet, VecDeque}; use convert_case::{Case, Casing}; use dap::TelemetrySpawnLocation; use display_map::*; -use edit_prediction::{EditPredictionProvider, EditPredictionProviderHandle}; +pub use display_map::{ChunkRenderer, ChunkRendererContext, DisplayPoint, FoldPlaceholder}; +pub use editor_settings::{ + CurrentLineHighlight, DocumentColorsRenderMode, EditorSettings, HideMouseMode, + ScrollBeyondLastLine, ScrollbarAxes, SearchSettings, ShowMinimap, ShowScrollbar, +}; use editor_settings::{GoToDefinitionFallback, Minimap as MinimapSettings}; +pub use editor_settings_controls::*; use element::{AcceptEditPredictionBinding, LineWithInvisibles, PositionMap, layout_line}; +pub use element::{ + CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition, +}; use futures::{ FutureExt, StreamExt as _, future::{self, Shared, join}, stream::FuturesUnordered, }; use fuzzy::{StringMatch, StringMatchCandidate}; +use lsp_colors::LspColorData; + +use ::git::blame::BlameEntry; +use ::git::{Restore, blame::ParsedCommitMessage}; +use code_context_menus::{ + AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu, + CompletionsMenu, ContextMenuOrigin, +}; use git::blame::{GitBlame, GlobalBlameRenderer}; use gpui::{ Action, Animation, AnimationExt, AnyElement, App, AppContext, AsyncWindowContext, @@ -115,42 +100,32 @@ use gpui::{ }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_links::{HoverLink, HoveredLinkState, InlayHighlight, find_file}; +pub use hover_popover::hover_markdown_style; use hover_popover::{HoverState, hide_hover}; use indent_guides::ActiveIndentGuidesState; use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy}; -use itertools::{Either, Itertools}; +pub use inline_completion::Direction; +use inline_completion::{EditPredictionProvider, InlineCompletionProviderHandle}; +pub use items::MAX_TAB_TITLE_LEN; +use itertools::Itertools; use language::{ - AutoindentMode, BlockCommentConfig, BracketMatch, BracketPair, Buffer, BufferRow, - BufferSnapshot, Capability, CharClassifier, CharKind, CodeLabel, CursorShape, DiagnosticEntry, - DiffOptions, EditPredictionsMode, EditPreview, HighlightedText, IndentKind, IndentSize, - Language, OffsetRangeExt, Point, Runnable, RunnableRange, Selection, SelectionGoal, TextObject, - TransactionId, TreeSitterOptions, WordsQuery, + AutoindentMode, BlockCommentConfig, BracketMatch, BracketPair, Buffer, Capability, CharKind, + CodeLabel, CursorShape, DiagnosticEntry, DiffOptions, EditPredictionsMode, EditPreview, + HighlightedText, IndentKind, IndentSize, Language, OffsetRangeExt, Point, Selection, + SelectionGoal, TextObject, TransactionId, TreeSitterOptions, WordsQuery, language_settings::{ self, InlayHintSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode, all_language_settings, language_settings, }, - point_from_lsp, point_to_lsp, text_diff_with_options, + point_from_lsp, text_diff_with_options, }; +use language::{BufferRow, CharClassifier, Runnable, RunnableRange, point_to_lsp}; use linked_editing_ranges::refresh_linked_ranges; -use lsp::{ - CodeActionKind, CompletionItemKind, CompletionTriggerKind, InsertTextFormat, InsertTextMode, - LanguageServerId, -}; -use lsp_colors::LspColorData; use markdown::Markdown; use mouse_context_menu::MouseContextMenu; -use movement::TextLayoutDetails; -use multi_buffer::{ - ExcerptInfo, ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow, - MultiOrSingleBufferOffsetRange, ToOffsetUtf16, -}; -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, CompletionResponse, ProjectPath, debugger::{ breakpoint_store::{ BreakpointEditAction, BreakpointSessionState, BreakpointState, BreakpointStore, @@ -159,12 +134,44 @@ use project::{ session::{Session, SessionEvent}, }, git_store::{GitStoreEvent, RepositoryEvent}, - lsp_store::{CompletionDocumentation, FormatTrigger, LspFormatTarget, OpenLspBufferHandle}, project_settings::{DiagnosticSeverity, GoToDiagnosticSeverityFilter}, +}; + +pub use git::blame::BlameRenderer; +pub use proposed_changes_editor::{ + ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar, +}; +use std::{cell::OnceCell, iter::Peekable, ops::Not}; +use task::{ResolvedTask, RunnableTag, TaskTemplate, TaskVariables}; + +pub use lsp::CompletionContext; +use lsp::{ + CodeActionKind, CompletionItemKind, CompletionTriggerKind, InsertTextFormat, InsertTextMode, + LanguageServerId, LanguageServerName, +}; + +use language::BufferSnapshot; +pub use lsp_ext::lsp_tasks; +use movement::TextLayoutDetails; +pub use multi_buffer::{ + Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, PathKey, + RowInfo, ToOffset, ToPoint, +}; +use multi_buffer::{ + ExcerptInfo, ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow, + MultiOrSingleBufferOffsetRange, ToOffsetUtf16, +}; +use parking_lot::Mutex; +use project::{ + CodeAction, Completion, CompletionIntent, CompletionSource, DocumentHighlight, InlayHint, + Location, LocationLink, PrepareRenameResponse, Project, ProjectItem, ProjectTransaction, + TaskSourceKind, + debugger::breakpoint_store::Breakpoint, + lsp_store::{CompletionDocumentation, FormatTrigger, LspFormatTarget, OpenLspBufferHandle}, project_settings::{GitGutterSetting, ProjectSettings}, }; -use rand::{seq::SliceRandom, thread_rng}; -use rpc::{ErrorCode, ErrorExt, proto::PeerId}; +use rand::prelude::*; +use rpc::{ErrorExt, proto::*}; use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide}; use selections_collection::{ MutableSelectionsCollection, SelectionsCollection, resolve_selections, @@ -173,24 +180,21 @@ use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsLocation, SettingsStore, update_settings_file}; use smallvec::{SmallVec, smallvec}; use snippet::Snippet; +use std::sync::Arc; use std::{ any::TypeId, borrow::Cow, - cell::OnceCell, cell::RefCell, cmp::{self, Ordering, Reverse}, - iter::Peekable, mem, num::NonZeroU32, - ops::Not, ops::{ControlFlow, Deref, DerefMut, Range, RangeInclusive}, path::{Path, PathBuf}, rc::Rc, - sync::Arc, time::{Duration, Instant}, }; +pub use sum_tree::Bias; use sum_tree::TreeMap; -use task::{ResolvedTask, RunnableTag, TaskTemplate, TaskVariables}; use text::{BufferId, FromAnchor, OffsetUtf16, Rope}; use theme::{ ActiveTheme, PlayerColor, StatusColors, SyntaxTheme, Theme, ThemeSettings, @@ -209,11 +213,14 @@ use workspace::{ notifications::{DetachAndPromptErr, NotificationId, NotifyTaskExt}, searchable::SearchEvent, }; +use zed_actions; use crate::{ code_context_menus::CompletionsMenuSource, - editor_settings::MultiCursorModifier, hover_links::{find_url, find_url_from_range}, +}; +use crate::{ + editor_settings::MultiCursorModifier, signature_help::{SignatureHelpHiddenBy, SignatureHelpState}, }; @@ -250,22 +257,6 @@ pub type RenderDiffHunkControlsFn = Arc< ) -> AnyElement, >; -enum ReportEditorEvent { - Saved { auto_saved: bool }, - EditorOpened, - Closed, -} - -impl ReportEditorEvent { - pub fn event_type(&self) -> &'static str { - match self { - Self::Saved { .. } => "Editor Saved", - Self::EditorOpened => "Editor Opened", - Self::Closed => "Editor Closed", - } - } -} - struct InlineValueCache { enabled: bool, inlays: Vec, @@ -284,7 +275,7 @@ impl InlineValueCache { #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum InlayId { - EditPrediction(usize), + InlineCompletion(usize), DebuggerValue(usize), // LSP Hint(usize), @@ -294,7 +285,7 @@ pub enum InlayId { impl InlayId { fn id(&self) -> usize { match self { - Self::EditPrediction(id) => *id, + Self::InlineCompletion(id) => *id, Self::DebuggerValue(id) => *id, Self::Hint(id) => *id, Self::Color(id) => *id, @@ -563,7 +554,7 @@ pub struct EditorStyle { pub syntax: Arc, pub status: StatusColors, pub inlay_hints_style: HighlightStyle, - pub edit_prediction_styles: EditPredictionStyles, + pub inline_completion_styles: InlineCompletionStyles, pub unnecessary_code_fade: f32, pub show_underlines: bool, } @@ -582,7 +573,7 @@ impl Default for EditorStyle { // style and retrieve them directly from the theme. status: StatusColors::dark(), inlay_hints_style: HighlightStyle::default(), - edit_prediction_styles: EditPredictionStyles { + inline_completion_styles: InlineCompletionStyles { insertion: HighlightStyle::default(), whitespace: HighlightStyle::default(), }, @@ -604,8 +595,8 @@ pub fn make_inlay_hints_style(cx: &mut App) -> HighlightStyle { } } -pub fn make_suggestion_styles(cx: &mut App) -> EditPredictionStyles { - EditPredictionStyles { +pub fn make_suggestion_styles(cx: &mut App) -> InlineCompletionStyles { + InlineCompletionStyles { insertion: HighlightStyle { color: Some(cx.theme().status().predictive), ..HighlightStyle::default() @@ -625,7 +616,7 @@ pub(crate) enum EditDisplayMode { Inline, } -enum EditPrediction { +enum InlineCompletion { Edit { edits: Vec<(Range, String)>, edit_preview: Option, @@ -638,9 +629,9 @@ enum EditPrediction { }, } -struct EditPredictionState { +struct InlineCompletionState { inlay_ids: Vec, - completion: EditPrediction, + completion: InlineCompletion, completion_id: Option, invalidation_range: Range, } @@ -653,7 +644,7 @@ enum EditPredictionSettings { }, } -enum EditPredictionHighlight {} +enum InlineCompletionHighlight {} #[derive(Debug, Clone)] struct InlineDiagnostic { @@ -664,7 +655,7 @@ struct InlineDiagnostic { severity: lsp::DiagnosticSeverity, } -pub enum MenuEditPredictionsPolicy { +pub enum MenuInlineCompletionsPolicy { Never, ByProvider, } @@ -780,7 +771,10 @@ impl MinimapVisibility { } fn disabled(&self) -> bool { - matches!(*self, Self::Disabled) + match *self { + Self::Disabled => true, + _ => false, + } } fn settings_visibility(&self) -> bool { @@ -937,10 +931,10 @@ impl ChangeList { } pub fn invert_last_group(&mut self) { - if let Some(last) = self.changes.last_mut() - && let Some(current) = last.current.as_mut() - { - mem::swap(&mut last.original, current); + if let Some(last) = self.changes.last_mut() { + if let Some(current) = last.current.as_mut() { + mem::swap(&mut last.original, current); + } } } } @@ -1034,7 +1028,9 @@ pub struct Editor { inline_diagnostics: Vec<(Anchor, InlineDiagnostic)>, soft_wrap_mode_override: Option, hard_wrap: Option, - project: Option>, + + // TODO: make this a access method + pub project: Option>, semantics_provider: Option>, completion_provider: Option>, collaboration_hub: Option>, @@ -1098,15 +1094,15 @@ pub struct Editor { pending_mouse_down: Option>>>, gutter_hovered: bool, hovered_link_state: Option, - edit_prediction_provider: Option, + edit_prediction_provider: Option, code_action_providers: Vec>, - active_edit_prediction: Option, + active_inline_completion: Option, /// Used to prevent flickering as the user types while the menu is open - stale_edit_prediction_in_menu: Option, + stale_inline_completion_in_menu: Option, edit_prediction_settings: EditPredictionSettings, - edit_predictions_hidden_for_vim_mode: bool, - show_edit_predictions_override: Option, - menu_edit_predictions_policy: MenuEditPredictionsPolicy, + inline_completions_hidden_for_vim_mode: bool, + show_inline_completions_override: Option, + menu_inline_completions_policy: MenuInlineCompletionsPolicy, edit_prediction_preview: EditPredictionPreview, edit_prediction_indent_conflict: bool, edit_prediction_requires_modifier_in_indent_conflict: bool, @@ -1424,7 +1420,7 @@ impl SelectionHistory { if self .undo_stack .back() - .is_none_or(|e| e.selections != entry.selections) + .map_or(true, |e| e.selections != entry.selections) { self.undo_stack.push_back(entry); if self.undo_stack.len() > MAX_SELECTION_HISTORY_LEN { @@ -1437,7 +1433,7 @@ impl SelectionHistory { if self .redo_stack .back() - .is_none_or(|e| e.selections != entry.selections) + .map_or(true, |e| e.selections != entry.selections) { self.redo_stack.push_back(entry); if self.redo_stack.len() > MAX_SELECTION_HISTORY_LEN { @@ -1521,8 +1517,8 @@ pub struct RenameState { struct InvalidationStack(Vec); -struct RegisteredEditPredictionProvider { - provider: Arc, +struct RegisteredInlineCompletionProvider { + provider: Arc, _subscription: Subscription, } @@ -1852,166 +1848,114 @@ 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 && 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 { + 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::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::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)); + } + editor.update_lsp_data(true, None, 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::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::EntryRenamed(transaction) => { - let Some(workspace) = editor.workspace() else { - return; - }; - let Some(active_editor) = workspace.read(cx).active_item_as::(cx) - else { - return; - }; - if active_editor.entity_id() == cx.entity_id() { - let edited_buffers_already_open = { - let other_editors: Vec> = workspace - .read(cx) - .panes() - .iter() - .flat_map(|pane| pane.read(cx).items_of_type::()) - .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() - }, - ) - }) - }) - }; - - 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)); + _ => {} }, )); - }; - - project_subscriptions.push(cx.subscribe_in( - &project.read(cx).breakpoint_store(), - window, - |editor, _, event, window, cx| match event { - BreakpointStoreEvent::ClearDebugLines => { - editor.clear_row_highlights::(); - 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 + if let Some(task_inventory) = project + .read(cx) + .task_store() + .read(cx) + .task_inventory() + .cloned() { - 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.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::(); + 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| { + 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(), + ); + } + _ => {} + } + })); + } } let buffer_snapshot = buffer.read(cx).snapshot(cx); @@ -2032,12 +1976,14 @@ 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()), @@ -2097,7 +2043,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), @@ -2157,8 +2103,8 @@ impl Editor { pending_mouse_down: None, hovered_link_state: None, edit_prediction_provider: None, - active_edit_prediction: None, - stale_edit_prediction_in_menu: None, + active_inline_completion: None, + stale_inline_completion_in_menu: None, edit_prediction_preview: EditPredictionPreview::Inactive { released_too_fast: false, }, @@ -2177,9 +2123,9 @@ impl Editor { hovered_cursors: HashMap::default(), next_editor_action_id: EditorActionId::default(), editor_actions: Rc::default(), - edit_predictions_hidden_for_vim_mode: false, - show_edit_predictions_override: None, - menu_edit_predictions_policy: MenuEditPredictionsPolicy::ByProvider, + inline_completions_hidden_for_vim_mode: false, + show_inline_completions_override: None, + menu_inline_completions_policy: MenuInlineCompletionsPolicy::ByProvider, edit_prediction_settings: EditPredictionSettings::Disabled, edit_prediction_indent_conflict: false, edit_prediction_requires_modifier_in_indent_conflict: true, @@ -2364,15 +2310,15 @@ impl Editor { editor.go_to_active_debug_line(window, cx); - 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); + 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); + } } editor.minimap = @@ -2382,7 +2328,7 @@ impl Editor { } if editor.mode.is_full() { - editor.report_editor_event(ReportEditorEvent::EditorOpened, None, cx); + editor.report_editor_event("Editor Opened", None, cx); } editor @@ -2410,36 +2356,8 @@ impl Editor { .is_some_and(|menu| menu.context_menu.focus_handle(cx).is_focused(window)) } - pub fn is_range_selected(&mut self, range: &Range, cx: &mut Context) -> 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::(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) + self.key_context_internal(self.has_active_inline_completion(), window, cx) } fn key_context_internal( @@ -2451,7 +2369,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", @@ -2557,7 +2475,9 @@ impl Editor { .context_menu .borrow() .as_ref() - .is_some_and(|context| matches!(context, CodeContextMenu::Completions(_))); + .map_or(false, |context| { + matches!(context, CodeContextMenu::Completions(_)) + }); showing_completions || self.edit_prediction_requires_modifier() @@ -2588,7 +2508,7 @@ impl Editor { || binding .keystrokes() .first() - .is_some_and(|keystroke| keystroke.display_modifiers.modified()) + .map_or(false, |keystroke| keystroke.modifiers.modified()) })) } @@ -2691,10 +2611,6 @@ impl Editor { &self.buffer } - pub fn project(&self) -> Option<&Entity> { - self.project.as_ref() - } - pub fn workspace(&self) -> Option> { self.workspace.as_ref()?.0.upgrade() } @@ -2792,11 +2708,6 @@ impl Editor { self.completion_provider = provider; } - #[cfg(any(test, feature = "test-support"))] - pub fn completion_provider(&self) -> Option> { - self.completion_provider.clone() - } - pub fn semantics_provider(&self) -> Option> { self.semantics_provider.clone() } @@ -2813,16 +2724,17 @@ impl Editor { ) where T: EditPredictionProvider, { - self.edit_prediction_provider = provider.map(|provider| RegisteredEditPredictionProvider { - _subscription: cx.observe_in(&provider, window, |this, _, window, cx| { - if this.focus_handle.is_focused(window) { - this.update_visible_edit_prediction(window, cx); - } - }), - provider: Arc::new(provider), - }); + self.edit_prediction_provider = + provider.map(|provider| RegisteredInlineCompletionProvider { + _subscription: cx.observe_in(&provider, window, |this, _, window, cx| { + if this.focus_handle.is_focused(window) { + this.update_visible_inline_completion(window, cx); + } + }), + provider: Arc::new(provider), + }); self.update_edit_prediction_settings(cx); - self.refresh_edit_prediction(false, false, window, cx); + self.refresh_inline_completion(false, false, window, cx); } pub fn placeholder_text(&self) -> Option<&str> { @@ -2893,24 +2805,24 @@ impl Editor { self.input_enabled = input_enabled; } - pub fn set_edit_predictions_hidden_for_vim_mode( + pub fn set_inline_completions_hidden_for_vim_mode( &mut self, hidden: bool, window: &mut Window, cx: &mut Context, ) { - if hidden != self.edit_predictions_hidden_for_vim_mode { - self.edit_predictions_hidden_for_vim_mode = hidden; + if hidden != self.inline_completions_hidden_for_vim_mode { + self.inline_completions_hidden_for_vim_mode = hidden; if hidden { - self.update_visible_edit_prediction(window, cx); + self.update_visible_inline_completion(window, cx); } else { - self.refresh_edit_prediction(true, false, window, cx); + self.refresh_inline_completion(true, false, window, cx); } } } - pub fn set_menu_edit_predictions_policy(&mut self, value: MenuEditPredictionsPolicy) { - self.menu_edit_predictions_policy = value; + pub fn set_menu_inline_completions_policy(&mut self, value: MenuInlineCompletionsPolicy) { + self.menu_inline_completions_policy = value; } pub fn set_autoindent(&mut self, autoindent: bool) { @@ -2947,7 +2859,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if self.show_edit_predictions_override.is_some() { + if self.show_inline_completions_override.is_some() { self.set_show_edit_predictions(None, window, cx); } else { let show_edit_predictions = !self.edit_predictions_enabled(); @@ -2961,17 +2873,17 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.show_edit_predictions_override = show_edit_predictions; + self.show_inline_completions_override = show_edit_predictions; self.update_edit_prediction_settings(cx); if let Some(false) = show_edit_predictions { - self.discard_edit_prediction(false, cx); + self.discard_inline_completion(false, cx); } else { - self.refresh_edit_prediction(false, true, window, cx); + self.refresh_inline_completion(false, true, window, cx); } } - fn edit_predictions_disabled_in_scope( + fn inline_completions_disabled_in_scope( &self, buffer: &Entity, buffer_position: language::Anchor, @@ -2984,7 +2896,7 @@ impl Editor { return false; }; - scope.override_name().is_some_and(|scope_name| { + scope.override_name().map_or(false, |scope_name| { settings .edit_predictions_disabled_in .iter() @@ -3074,19 +2986,20 @@ impl Editor { } if local { - 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), - ); - }) + 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), + ); + }) + } + } } let mut context_menu = self.context_menu.borrow_mut(); @@ -3101,28 +3014,28 @@ impl Editor { let completion_position = completion_menu.map(|menu| menu.initial_position); drop(context_menu); - 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) + 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 { - // 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 - }; + false + }; - if continue_showing { - self.show_completions(&ShowCompletions { trigger: None }, window, cx); - } else { - self.hide_context_menu(window, cx); + if continue_showing { + self.show_completions(&ShowCompletions { trigger: None }, window, cx); + } else { + self.hide_context_menu(window, cx); + } } } @@ -3137,7 +3050,7 @@ impl Editor { self.refresh_document_highlights(cx); self.refresh_selected_text_highlights(false, window, cx); refresh_matching_bracket_highlights(self, window, cx); - self.update_visible_edit_prediction(window, cx); + self.update_visible_inline_completion(window, cx); self.edit_prediction_requires_modifier_in_indent_conflict = true; linked_editing_ranges::refresh_linked_ranges(self, window, cx); self.inline_blame_popover.take(); @@ -3153,27 +3066,30 @@ impl Editor { if selections.len() == 1 { cx.emit(SearchEvent::ActiveMatchChanged) } - 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 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 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 { + 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 { background_executor.timer(SERIALIZATION_THROTTLE_TIME).await; let db_selections = selections .iter() @@ -3190,6 +3106,8 @@ impl Editor { .with_context(|| format!("persisting editor selections for editor {editor_id}, workspace {workspace_id:?}")) .log_err(); }); + } + } } } @@ -3266,31 +3184,35 @@ impl Editor { 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; + 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); + }); } - this.selections.change_with(cx, |selections| { - selections.select_anchors(other_selections); - }); - } - }); + _ => {} + }); - let this_subscription = cx.subscribe_self::(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; + let this_subscription = + cx.subscribe_self::(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); + }) + }); } - other.update(cx, |other_editor, cx| { - other_editor.selections.change_with(cx, |selections| { - selections.select_anchors(these_selections); - }) - }); - } - }); + _ => {} + }); Subscription::join(other_subscription, this_subscription) } @@ -3371,9 +3293,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); } } @@ -3793,9 +3715,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) } }; @@ -3918,7 +3840,7 @@ impl Editor { return true; } - if is_user_requested && self.discard_edit_prediction(true, cx) { + if is_user_requested && self.discard_inline_completion(true, cx) { return true; } @@ -4072,18 +3994,18 @@ impl Editor { let following_text_allows_autoclose = snapshot .chars_at(selection.start) .next() - .is_none_or(|c| scope.should_autoclose_before(c)); + .map_or(true, |c| scope.should_autoclose_before(c)); let preceding_text_allows_autoclose = selection.start.column == 0 - || snapshot - .reversed_chars_at(selection.start) - .next() - .is_none_or(|c| { + || snapshot.reversed_chars_at(selection.start).next().map_or( + true, + |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 @@ -4183,38 +4105,42 @@ impl Editor { if self.auto_replace_emoji_shortcode && selection.is_empty() && text.as_ref().ends_with(':') - && let Some(possible_emoji_short_code) = - Self::find_possible_emoji_shortcode_at_position(&snapshot, selection.start) - && !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, - ); + if 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, + ); - // 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 @@ -4224,7 +4150,7 @@ impl Editor { if !self.linked_edit_ranges.is_empty() { let start_anchor = snapshot.anchor_before(selection.start); - let is_word_char = text.chars().next().is_none_or(|char| { + let is_word_char = text.chars().next().map_or(true, |char| { let classifier = snapshot .char_classifier_at(start_anchor.to_offset(&snapshot)) .ignore_punctuation(true); @@ -4320,7 +4246,7 @@ impl Editor { ); } - let had_active_edit_prediction = this.has_active_edit_prediction(); + let had_active_inline_completion = this.has_active_inline_completion(); this.change_selections( SelectionEffects::scroll(Autoscroll::fit()).completions(false), window, @@ -4328,11 +4254,12 @@ impl Editor { |s| s.select(new_selections), ); - if !bracket_inserted - && let Some(on_type_format_task) = + if !bracket_inserted { + if let Some(on_type_format_task) = this.trigger_on_type_formatting(text.to_string(), window, cx) - { - on_type_format_task.detach_and_log_err(cx); + { + on_type_format_task.detach_and_log_err(cx); + } } let editor_settings = EditorSettings::get_global(cx); @@ -4344,7 +4271,7 @@ impl Editor { } let trigger_in_words = - this.show_edit_predictions_in_menu() || !had_active_edit_prediction; + this.show_edit_predictions_in_menu() || !had_active_inline_completion; if this.hard_wrap.is_some() { let latest: Range = this.selections.newest(cx).range(); if latest.is_empty() @@ -4366,7 +4293,7 @@ impl Editor { } this.trigger_completion_on_input(&text, trigger_in_words, window, cx); linked_editing_ranges::refresh_linked_ranges(this, window, cx); - this.refresh_edit_prediction(true, false, window, cx); + this.refresh_inline_completion(true, false, window, cx); jsx_tag_auto_close::handle_from(this, initial_buffer_versions, window, cx); }); } @@ -4578,7 +4505,7 @@ impl Editor { let mut char_position = 0u32; let mut end_tag_offset = None; - 'outer: for chunk in snapshot.text_for_range(range) { + 'outer: for chunk in snapshot.text_for_range(range.clone()) { if let Some(byte_pos) = chunk.find(&**end_tag) { let chars_before_match = chunk[..byte_pos].chars().count() as u32; @@ -4701,7 +4628,7 @@ impl Editor { .collect(); this.change_selections(Default::default(), window, cx, |s| s.select(new_selections)); - this.refresh_edit_prediction(true, false, window, cx); + this.refresh_inline_completion(true, false, window, cx); }); } @@ -4928,7 +4855,11 @@ impl Editor { cx: &mut Context, ) -> bool { let position = self.selections.newest_anchor().head(); - let Some(buffer) = self.buffer.read(cx).buffer_for_anchor(position, cx) else { + let multibuffer = self.buffer.read(cx); + let Some(buffer) = position + .buffer_id + .and_then(|buffer_id| multibuffer.buffer(buffer_id).clone()) + else { return false; }; @@ -5262,7 +5193,7 @@ impl Editor { restrict_to_languages: Option<&HashSet>>, cx: &mut Context, ) -> HashMap, clock::Global, Range)> { - let Some(project) = self.project() else { + let Some(project) = self.project.as_ref() else { return HashMap::default(); }; let project = project.read(cx); @@ -5294,10 +5225,10 @@ impl Editor { } let language = buffer.language()?; - if let Some(restrict_to_languages) = restrict_to_languages - && !restrict_to_languages.contains(language) - { - return None; + if let Some(restrict_to_languages) = restrict_to_languages { + if !restrict_to_languages.contains(language) { + return None; + } } Some(( excerpt_id, @@ -5344,7 +5275,7 @@ impl Editor { return None; } - let project = self.project()?; + let project = self.project.as_ref()?; let position = self.selections.newest_anchor().head(); let (buffer, buffer_position) = self .buffer @@ -5462,11 +5393,11 @@ impl Editor { let sort_completions = provider .as_ref() - .is_some_and(|provider| provider.sort_completions()); + .map_or(false, |provider| provider.sort_completions()); let filter_completions = provider .as_ref() - .is_none_or(|provider| provider.filter_completions()); + .map_or(true, |provider| provider.filter_completions()); let trigger_kind = match trigger { Some(trigger) if buffer.read(cx).completion_triggers().contains(trigger) => { @@ -5572,12 +5503,7 @@ impl Editor { let skip_digits = query .as_ref() - .is_none_or(|query| !query.chars().any(|c| c.is_digit(10))); - - let omit_word_completions = match &query { - Some(query) => query.chars().count() < completion_settings.words_min_length, - None => completion_settings.words_min_length != 0, - }; + .map_or(true, |query| !query.chars().any(|c| c.is_digit(10))); let (mut words, provider_responses) = match &provider { Some(provider) => { @@ -5590,11 +5516,9 @@ impl Editor { cx, ); - let words = match (omit_word_completions, completion_settings.words) { - (true, _) | (_, WordsCompletionMode::Disabled) => { - Task::ready(BTreeMap::default()) - } - (false, WordsCompletionMode::Enabled | WordsCompletionMode::Fallback) => cx + let words = match completion_settings.words { + WordsCompletionMode::Disabled => Task::ready(BTreeMap::default()), + WordsCompletionMode::Enabled | WordsCompletionMode::Fallback => cx .background_spawn(async move { buffer_snapshot.words_in_range(WordsQuery { fuzzy_contents: None, @@ -5606,20 +5530,16 @@ impl Editor { (words, provider_responses) } - None => { - let words = if omit_word_completions { - Task::ready(BTreeMap::default()) - } else { - cx.background_spawn(async move { - buffer_snapshot.words_in_range(WordsQuery { - fuzzy_contents: None, - range: word_search_range, - skip_digits, - }) + None => ( + cx.background_spawn(async move { + buffer_snapshot.words_in_range(WordsQuery { + fuzzy_contents: None, + range: word_search_range, + skip_digits, }) - }; - (words, Task::ready(Ok(Vec::new()))) - } + }), + Task::ready(Ok(Vec::new())), + ), }; let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order; @@ -5636,15 +5556,15 @@ impl Editor { // that having one source with `is_incomplete: true` doesn't cause all to be re-queried. let mut completions = Vec::new(); let mut is_incomplete = false; - if let Some(provider_responses) = provider_responses.await.log_err() - && !provider_responses.is_empty() - { - for response in provider_responses { - completions.extend(response.completions); - is_incomplete = is_incomplete || response.is_incomplete; - } - if completion_settings.words == WordsCompletionMode::Fallback { - words = Task::ready(BTreeMap::default()); + if let Some(provider_responses) = provider_responses.await.log_err() { + if !provider_responses.is_empty() { + for response in provider_responses { + completions.extend(response.completions); + is_incomplete = is_incomplete || response.is_incomplete; + } + if completion_settings.words == WordsCompletionMode::Fallback { + words = Task::ready(BTreeMap::default()); + } } } @@ -5709,31 +5629,34 @@ impl Editor { let Ok(()) = editor.update_in(cx, |editor, window, cx| { // Newer menu already set, so exit. - if let Some(CodeContextMenu::Completions(prev_menu)) = - editor.context_menu.borrow().as_ref() - && prev_menu.id > id - { - return; + match editor.context_menu.borrow().as_ref() { + Some(CodeContextMenu::Completions(prev_menu)) => { + if prev_menu.id > id { + return; + } + } + _ => {} }; // Only valid to take prev_menu because it the new menu is immediately set // below, or the menu is hidden. - if let Some(CodeContextMenu::Completions(prev_menu)) = - editor.context_menu.borrow_mut().take() - { - let position_matches = - if prev_menu.initial_position == menu.initial_position { - true - } else { - let snapshot = editor.buffer.read(cx).read(cx); - prev_menu.initial_position.to_offset(&snapshot) - == menu.initial_position.to_offset(&snapshot) - }; - if position_matches { - // Preserve markdown cache before `set_filter_results` because it will - // try to populate the documentation cache. - menu.preserve_markdown_cache(prev_menu); + match editor.context_menu.borrow_mut().take() { + Some(CodeContextMenu::Completions(prev_menu)) => { + let position_matches = + if prev_menu.initial_position == menu.initial_position { + true + } else { + let snapshot = editor.buffer.read(cx).read(cx); + prev_menu.initial_position.to_offset(&snapshot) + == menu.initial_position.to_offset(&snapshot) + }; + if position_matches { + // Preserve markdown cache before `set_filter_results` because it will + // try to populate the documentation cache. + menu.preserve_markdown_cache(prev_menu); + } } + _ => {} }; menu.set_filter_results(matches, provider, window, cx); @@ -5746,30 +5669,30 @@ impl Editor { editor .update_in(cx, |editor, window, cx| { - if editor.focus_handle.is_focused(window) - && let Some(menu) = menu - { - *editor.context_menu.borrow_mut() = - Some(CodeContextMenu::Completions(menu)); + if editor.focus_handle.is_focused(window) { + if let Some(menu) = menu { + *editor.context_menu.borrow_mut() = + Some(CodeContextMenu::Completions(menu)); - crate::hover_popover::hide_hover(editor, cx); - if editor.show_edit_predictions_in_menu() { - editor.update_visible_edit_prediction(window, cx); - } else { - editor.discard_edit_prediction(false, cx); + crate::hover_popover::hide_hover(editor, cx); + if editor.show_edit_predictions_in_menu() { + editor.update_visible_inline_completion(window, cx); + } else { + editor.discard_inline_completion(false, cx); + } + + cx.notify(); + return; } - - cx.notify(); - return; } if editor.completion_tasks.len() <= 1 { // If there are no more completion tasks and the last menu was empty, we should hide it. let was_hidden = editor.hide_context_menu(window, cx).is_none(); - // If it was already hidden and we don't show edit predictions in the menu, - // we should also show the edit prediction when available. + // If it was already hidden and we don't show inline completions in the menu, we should + // also show the inline-completion when available. if was_hidden && editor.show_edit_predictions_in_menu() { - editor.update_visible_edit_prediction(window, cx); + editor.update_visible_inline_completion(window, cx); } } }) @@ -5863,7 +5786,7 @@ impl Editor { let entries = completions_menu.entries.borrow(); let mat = entries.get(item_ix.unwrap_or(completions_menu.selected_item))?; if self.show_edit_predictions_in_menu() { - self.discard_edit_prediction(true, cx); + self.discard_inline_completion(true, cx); } mat.candidate_id }; @@ -5903,7 +5826,7 @@ impl Editor { multibuffer_anchor.start.to_offset(&snapshot) ..multibuffer_anchor.end.to_offset(&snapshot) }; - if snapshot.buffer_id_for_anchor(newest_anchor.head()) != Some(buffer.remote_id()) { + if newest_anchor.head().buffer_id != Some(buffer.remote_id()) { return None; } @@ -6007,14 +5930,14 @@ impl Editor { }) } - editor.refresh_edit_prediction(true, false, window, cx); + editor.refresh_inline_completion(true, false, window, cx); }); self.invalidate_autoclose_regions(&self.selections.disjoint_anchors(), &snapshot); let show_new_completions_on_confirm = completion .confirm .as_ref() - .is_some_and(|confirm| confirm(intent, window, cx)); + .map_or(false, |confirm| confirm(intent, window, cx)); if show_new_completions_on_confirm { self.show_completions(&ShowCompletions { trigger: None }, window, cx); } @@ -6067,7 +5990,7 @@ impl Editor { let deployed_from = action.deployed_from.clone(); let action = action.clone(); self.completion_tasks.clear(); - self.discard_edit_prediction(false, cx); + self.discard_inline_completion(false, cx); let multibuffer_point = match &action.deployed_from { Some(CodeActionSource::Indicator(row)) | Some(CodeActionSource::RunMenu(row)) => { @@ -6107,11 +6030,11 @@ impl Editor { Some(CodeActionSource::Indicator(_)) => Task::ready(Ok(Default::default())), _ => { let mut task_context_task = Task::ready(None); - if let Some(tasks) = &tasks - && let Some(project) = project - { - task_context_task = - Self::build_tasks_context(&project, &buffer, buffer_row, tasks, cx); + if let Some(tasks) = &tasks { + if let Some(project) = project { + task_context_task = + Self::build_tasks_context(&project, &buffer, buffer_row, &tasks, cx); + } } cx.spawn_in(window, { @@ -6146,10 +6069,10 @@ impl Editor { let spawn_straight_away = quick_launch && resolved_tasks .as_ref() - .is_some_and(|tasks| tasks.templates.len() == 1) + .map_or(false, |tasks| tasks.templates.len() == 1) && code_actions .as_ref() - .is_none_or(|actions| actions.is_empty()) + .map_or(true, |actions| actions.is_empty()) && debug_scenarios.is_empty(); editor.update_in(cx, |editor, window, cx| { @@ -6176,14 +6099,14 @@ impl Editor { deployed_from, })); cx.notify(); - if spawn_straight_away - && let Some(task) = editor.confirm_code_action( + if spawn_straight_away { + if let Some(task) = editor.confirm_code_action( &ConfirmCodeAction { item_ix: Some(0) }, window, cx, - ) - { - return task; + ) { + return task; + } } Task::ready(Ok(())) @@ -6199,7 +6122,7 @@ impl Editor { cx: &mut App, ) -> Task> { maybe!({ - let project = self.project()?; + let project = self.project.as_ref()?; let dap_store = project.read(cx).dap_store(); let mut scenarios = vec![]; let resolved_tasks = resolved_tasks.as_ref()?; @@ -6224,11 +6147,12 @@ impl Editor { } }); Some(cx.background_spawn(async move { - futures::future::join_all(scenarios) + let scenarios = futures::future::join_all(scenarios) .await .into_iter() .flatten() - .collect::>() + .collect::>(); + scenarios })) }) .unwrap_or_else(|| Task::ready(vec![])) @@ -6326,7 +6250,7 @@ impl Editor { })) } CodeActionsItem::DebugScenario(scenario) => { - let context = actions_menu.actions.context; + let context = actions_menu.actions.context.clone(); workspace.update(cx, |workspace, cx| { dap::send_telemetry(&scenario, TelemetrySpawnLocation::Gutter, cx); @@ -6345,7 +6269,7 @@ impl Editor { } pub async fn open_project_transaction( - editor: &WeakEntity, + this: &WeakEntity, workspace: WeakEntity, transaction: ProjectTransaction, title: String, @@ -6363,26 +6287,27 @@ impl Editor { if let Some((buffer, transaction)) = entries.first() { if entries.len() == 1 { - let excerpt = editor.update(cx, |editor, cx| { + let excerpt = this.update(cx, |editor, cx| { editor .buffer() .read(cx) .excerpt_containing(editor.selections.newest_anchor().head(), cx) })?; - if let Some((_, excerpted_buffer, excerpt_range)) = excerpt - && excerpted_buffer == *buffer - { - let all_edits_within_excerpt = buffer.read_with(cx, |buffer, _| { - let excerpt_range = excerpt_range.to_offset(buffer); - buffer - .edited_ranges_for_transaction::(transaction) - .all(|range| { - excerpt_range.start <= range.start && excerpt_range.end >= range.end - }) - })?; + if let Some((_, excerpted_buffer, excerpt_range)) = excerpt { + if excerpted_buffer == *buffer { + let all_edits_within_excerpt = buffer.read_with(cx, |buffer, _| { + let excerpt_range = excerpt_range.to_offset(buffer); + buffer + .edited_ranges_for_transaction::(transaction) + .all(|range| { + excerpt_range.start <= range.start + && excerpt_range.end >= range.end + }) + })?; - if all_edits_within_excerpt { - return Ok(()); + if all_edits_within_excerpt { + return Ok(()); + } } } } @@ -6485,6 +6410,7 @@ impl Editor { IconButton::new("inline_code_actions", ui::IconName::BoltFilled) .icon_size(icon_size) .shape(ui::IconButtonShape::Square) + .style(ButtonStyle::Transparent) .icon_color(ui::Color::Hidden) .toggle_state(is_active) .when(show_tooltip, |this| { @@ -6526,7 +6452,7 @@ impl Editor { fn refresh_code_actions(&mut self, window: &mut Window, cx: &mut Context) -> Option<()> { let newest_selection = self.selections.newest_anchor().clone(); - let newest_selection_adjusted = self.selections.newest_adjusted(cx); + let newest_selection_adjusted = self.selections.newest_adjusted(cx).clone(); let buffer = self.buffer.read(cx); if newest_selection.head().diff_base_anchor.is_some() { return None; @@ -6760,10 +6686,11 @@ impl Editor { return; } + let buffer_id = cursor_position.buffer_id; let buffer = this.buffer.read(cx); - if buffer + if !buffer .text_anchor_for_position(cursor_position, cx) - .is_none_or(|(buffer, _)| buffer != cursor_buffer) + .map_or(false, |(buffer, _)| buffer == cursor_buffer) { return; } @@ -6772,8 +6699,8 @@ impl Editor { let mut write_ranges = Vec::new(); let mut read_ranges = Vec::new(); for highlight in highlights { - let buffer_id = cursor_buffer.read(cx).remote_id(); - for (excerpt_id, excerpt_range) in buffer.excerpts_for_buffer(buffer_id, cx) + for (excerpt_id, excerpt_range) in + buffer.excerpts_for_buffer(cursor_buffer.read(cx).remote_id(), cx) { let start = highlight .range @@ -6788,12 +6715,12 @@ impl Editor { } let range = Anchor { - buffer_id: Some(buffer_id), + buffer_id, excerpt_id, text_anchor: start, diff_base_anchor: None, }..Anchor { - buffer_id: Some(buffer_id), + buffer_id, excerpt_id, text_anchor: end, diff_base_anchor: None, @@ -6828,7 +6755,7 @@ impl Editor { &mut self, cx: &mut Context, ) -> Option<(String, Range)> { - if matches!(self.mode, EditorMode::SingleLine) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { return None; } if !EditorSettings::get_global(cx).selection_highlight { @@ -6889,7 +6816,7 @@ impl Editor { for (buffer_snapshot, search_range, excerpt_id) in buffer_ranges { match_ranges.extend( regex - .search(buffer_snapshot, Some(search_range.clone())) + .search(&buffer_snapshot, Some(search_range.clone())) .await .into_iter() .filter_map(|match_range| { @@ -7013,7 +6940,9 @@ impl Editor { || self .quick_selection_highlight_task .as_ref() - .is_none_or(|(prev_anchor_range, _)| prev_anchor_range != &query_range) + .map_or(true, |(prev_anchor_range, _)| { + prev_anchor_range != &query_range + }) { let multi_buffer_visible_start = self .scroll_manager @@ -7042,7 +6971,9 @@ impl Editor { || self .debounced_selection_highlight_task .as_ref() - .is_none_or(|(prev_anchor_range, _)| prev_anchor_range != &query_range) + .map_or(true, |(prev_anchor_range, _)| { + prev_anchor_range != &query_range + }) { let multi_buffer_start = multi_buffer_snapshot .anchor_before(0) @@ -7065,24 +6996,20 @@ impl Editor { } } - pub fn refresh_edit_prediction( + pub fn refresh_inline_completion( &mut self, debounce: bool, user_requested: bool, window: &mut Window, cx: &mut Context, ) -> Option<()> { - if DisableAiSettings::get_global(cx).disable_ai { - return None; - } - let provider = self.edit_prediction_provider()?; let cursor = self.selections.newest_anchor().head(); let (buffer, cursor_buffer_position) = self.buffer.read(cx).text_anchor_for_position(cursor, cx)?; if !self.edit_predictions_enabled_in_buffer(&buffer, cursor_buffer_position, cx) { - self.discard_edit_prediction(false, cx); + self.discard_inline_completion(false, cx); return None; } @@ -7091,11 +7018,11 @@ impl Editor { || !self.is_focused(window) || buffer.read(cx).is_empty()) { - self.discard_edit_prediction(false, cx); + self.discard_inline_completion(false, cx); return None; } - self.update_visible_edit_prediction(window, cx); + self.update_visible_inline_completion(window, cx); provider.refresh( self.project.clone(), buffer, @@ -7133,7 +7060,6 @@ impl Editor { pub fn update_edit_prediction_settings(&mut self, cx: &mut Context) { if self.edit_prediction_provider.is_none() || DisableAiSettings::get_global(cx).disable_ai { self.edit_prediction_settings = EditPredictionSettings::Disabled; - self.discard_edit_prediction(false, cx); } else { let selection = self.selections.newest_anchor(); let cursor = selection.head(); @@ -7154,8 +7080,8 @@ impl Editor { cx: &App, ) -> EditPredictionSettings { if !self.mode.is_full() - || !self.show_edit_predictions_override.unwrap_or(true) - || self.edit_predictions_disabled_in_scope(buffer, buffer_position, cx) + || !self.show_inline_completions_override.unwrap_or(true) + || self.inline_completions_disabled_in_scope(buffer, buffer_position, cx) { return EditPredictionSettings::Disabled; } @@ -7169,15 +7095,17 @@ impl Editor { }; let by_provider = matches!( - self.menu_edit_predictions_policy, - MenuEditPredictionsPolicy::ByProvider + self.menu_inline_completions_policy, + MenuInlineCompletionsPolicy::ByProvider ); let show_in_menu = by_provider && self .edit_prediction_provider .as_ref() - .is_some_and(|provider| provider.provider.show_completions_in_menu()); + .map_or(false, |provider| { + provider.provider.show_completions_in_menu() + }); let preview_requires_modifier = all_language_settings(file, cx).edit_predictions_mode() == EditPredictionsMode::Subtle; @@ -7225,7 +7153,7 @@ impl Editor { return Some(false); } let provider = self.edit_prediction_provider()?; - if !provider.is_enabled(buffer, buffer_position, cx) { + if !provider.is_enabled(&buffer, buffer_position, cx) { return Some(false); } let buffer = buffer.read(cx); @@ -7238,7 +7166,7 @@ impl Editor { .unwrap_or(false) } - fn cycle_edit_prediction( + fn cycle_inline_completion( &mut self, direction: Direction, window: &mut Window, @@ -7248,28 +7176,28 @@ impl Editor { let cursor = self.selections.newest_anchor().head(); let (buffer, cursor_buffer_position) = self.buffer.read(cx).text_anchor_for_position(cursor, cx)?; - if self.edit_predictions_hidden_for_vim_mode || !self.should_show_edit_predictions() { + if self.inline_completions_hidden_for_vim_mode || !self.should_show_edit_predictions() { return None; } provider.cycle(buffer, cursor_buffer_position, direction, cx); - self.update_visible_edit_prediction(window, cx); + self.update_visible_inline_completion(window, cx); Some(()) } - pub fn show_edit_prediction( + pub fn show_inline_completion( &mut self, _: &ShowEditPrediction, window: &mut Window, cx: &mut Context, ) { - if !self.has_active_edit_prediction() { - self.refresh_edit_prediction(false, true, window, cx); + if !self.has_active_inline_completion() { + self.refresh_inline_completion(false, true, window, cx); return; } - self.update_visible_edit_prediction(window, cx); + self.update_visible_inline_completion(window, cx); } pub fn display_cursor_names( @@ -7301,11 +7229,11 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if self.has_active_edit_prediction() { - self.cycle_edit_prediction(Direction::Next, window, cx); + if self.has_active_inline_completion() { + self.cycle_inline_completion(Direction::Next, window, cx); } else { let is_copilot_disabled = self - .refresh_edit_prediction(false, true, window, cx) + .refresh_inline_completion(false, true, window, cx) .is_none(); if is_copilot_disabled { cx.propagate(); @@ -7319,11 +7247,11 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if self.has_active_edit_prediction() { - self.cycle_edit_prediction(Direction::Prev, window, cx); + if self.has_active_inline_completion() { + self.cycle_inline_completion(Direction::Prev, window, cx); } else { let is_copilot_disabled = self - .refresh_edit_prediction(false, true, window, cx) + .refresh_inline_completion(false, true, window, cx) .is_none(); if is_copilot_disabled { cx.propagate(); @@ -7341,14 +7269,18 @@ impl Editor { self.hide_context_menu(window, cx); } - let Some(active_edit_prediction) = self.active_edit_prediction.as_ref() else { + let Some(active_inline_completion) = self.active_inline_completion.as_ref() else { return; }; - self.report_edit_prediction_event(active_edit_prediction.completion_id.clone(), true, cx); + self.report_inline_completion_event( + active_inline_completion.completion_id.clone(), + true, + cx, + ); - match &active_edit_prediction.completion { - EditPrediction::Move { target, .. } => { + match &active_inline_completion.completion { + InlineCompletion::Move { target, .. } => { let target = *target; if let Some(position_map) = &self.last_position_map { @@ -7390,7 +7322,7 @@ impl Editor { } } } - EditPrediction::Edit { edits, .. } => { + InlineCompletion::Edit { edits, .. } => { if let Some(provider) = self.edit_prediction_provider() { provider.accept(cx); } @@ -7418,9 +7350,9 @@ impl Editor { } } - self.update_visible_edit_prediction(window, cx); - if self.active_edit_prediction.is_none() { - self.refresh_edit_prediction(true, true, window, cx); + self.update_visible_inline_completion(window, cx); + if self.active_inline_completion.is_none() { + self.refresh_inline_completion(true, true, window, cx); } cx.notify(); @@ -7430,23 +7362,27 @@ impl Editor { self.edit_prediction_requires_modifier_in_indent_conflict = false; } - pub fn accept_partial_edit_prediction( + pub fn accept_partial_inline_completion( &mut self, _: &AcceptPartialEditPrediction, window: &mut Window, cx: &mut Context, ) { - let Some(active_edit_prediction) = self.active_edit_prediction.as_ref() else { + let Some(active_inline_completion) = self.active_inline_completion.as_ref() else { return; }; if self.selections.count() != 1 { return; } - self.report_edit_prediction_event(active_edit_prediction.completion_id.clone(), true, cx); + self.report_inline_completion_event( + active_inline_completion.completion_id.clone(), + true, + cx, + ); - match &active_edit_prediction.completion { - EditPrediction::Move { target, .. } => { + match &active_inline_completion.completion { + InlineCompletion::Move { target, .. } => { let target = *target; self.change_selections( SelectionEffects::scroll(Autoscroll::newest()), @@ -7457,7 +7393,7 @@ impl Editor { }, ); } - EditPrediction::Edit { edits, .. } => { + InlineCompletion::Edit { edits, .. } => { // Find an insertion that starts at the cursor position. let snapshot = self.buffer.read(cx).snapshot(cx); let cursor_offset = self.selections.newest::(cx).head(); @@ -7491,7 +7427,7 @@ impl Editor { self.insert_with_autoindent_mode(&partial_completion, None, window, cx); - self.refresh_edit_prediction(true, true, window, cx); + self.refresh_inline_completion(true, true, window, cx); cx.notify(); } else { self.accept_edit_prediction(&Default::default(), window, cx); @@ -7500,28 +7436,28 @@ impl Editor { } } - fn discard_edit_prediction( + fn discard_inline_completion( &mut self, - should_report_edit_prediction_event: bool, + should_report_inline_completion_event: bool, cx: &mut Context, ) -> bool { - if should_report_edit_prediction_event { + if should_report_inline_completion_event { let completion_id = self - .active_edit_prediction + .active_inline_completion .as_ref() .and_then(|active_completion| active_completion.completion_id.clone()); - self.report_edit_prediction_event(completion_id, false, cx); + self.report_inline_completion_event(completion_id, false, cx); } if let Some(provider) = self.edit_prediction_provider() { provider.discard(cx); } - self.take_active_edit_prediction(cx) + self.take_active_inline_completion(cx) } - fn report_edit_prediction_event(&self, id: Option, accepted: bool, cx: &App) { + fn report_inline_completion_event(&self, id: Option, accepted: bool, cx: &App) { let Some(provider) = self.edit_prediction_provider() else { return; }; @@ -7552,18 +7488,18 @@ impl Editor { ); } - pub fn has_active_edit_prediction(&self) -> bool { - self.active_edit_prediction.is_some() + pub fn has_active_inline_completion(&self) -> bool { + self.active_inline_completion.is_some() } - fn take_active_edit_prediction(&mut self, cx: &mut Context) -> bool { - let Some(active_edit_prediction) = self.active_edit_prediction.take() else { + fn take_active_inline_completion(&mut self, cx: &mut Context) -> bool { + let Some(active_inline_completion) = self.active_inline_completion.take() else { return false; }; - self.splice_inlays(&active_edit_prediction.inlay_ids, Default::default(), cx); - self.clear_highlights::(cx); - self.stale_edit_prediction_in_menu = Some(active_edit_prediction); + self.splice_inlays(&active_inline_completion.inlay_ids, Default::default(), cx); + self.clear_highlights::(cx); + self.stale_inline_completion_in_menu = Some(active_inline_completion); true } @@ -7686,16 +7622,16 @@ impl Editor { .keystroke() { modifiers_held = modifiers_held - || (&accept_keystroke.display_modifiers == modifiers - && accept_keystroke.display_modifiers.modified()); + || (&accept_keystroke.modifiers == modifiers + && accept_keystroke.modifiers.modified()); }; if let Some(accept_partial_keystroke) = self .accept_edit_prediction_keybind(true, window, cx) .keystroke() { modifiers_held = modifiers_held - || (&accept_partial_keystroke.display_modifiers == modifiers - && accept_partial_keystroke.display_modifiers.modified()); + || (&accept_partial_keystroke.modifiers == modifiers + && accept_partial_keystroke.modifiers.modified()); } if modifiers_held { @@ -7708,7 +7644,7 @@ impl Editor { since: Instant::now(), }; - self.update_visible_edit_prediction(window, cx); + self.update_visible_inline_completion(window, cx); cx.notify(); } } else if let EditPredictionPreview::Active { @@ -7731,20 +7667,16 @@ impl Editor { released_too_fast: since.elapsed() < Duration::from_millis(200), }; self.clear_row_highlights::(); - self.update_visible_edit_prediction(window, cx); + self.update_visible_inline_completion(window, cx); cx.notify(); } } - fn update_visible_edit_prediction( + fn update_visible_inline_completion( &mut self, _window: &mut Window, cx: &mut Context, ) -> Option<()> { - if DisableAiSettings::get_global(cx).disable_ai { - return None; - } - let selection = self.selections.newest_anchor(); let cursor = selection.head(); let multibuffer = self.buffer.read(cx).snapshot(cx); @@ -7754,24 +7686,24 @@ impl Editor { let show_in_menu = self.show_edit_predictions_in_menu(); let completions_menu_has_precedence = !show_in_menu && (self.context_menu.borrow().is_some() - || (!self.completion_tasks.is_empty() && !self.has_active_edit_prediction())); + || (!self.completion_tasks.is_empty() && !self.has_active_inline_completion())); if completions_menu_has_precedence || !offset_selection.is_empty() || self - .active_edit_prediction + .active_inline_completion .as_ref() - .is_some_and(|completion| { + .map_or(false, |completion| { let invalidation_range = completion.invalidation_range.to_offset(&multibuffer); let invalidation_range = invalidation_range.start..=invalidation_range.end; !invalidation_range.contains(&offset_selection.head()) }) { - self.discard_edit_prediction(false, cx); + self.discard_inline_completion(false, cx); return None; } - self.take_active_edit_prediction(cx); + self.take_active_inline_completion(cx); let Some(provider) = self.edit_prediction_provider() else { self.edit_prediction_settings = EditPredictionSettings::Disabled; return None; @@ -7783,11 +7715,6 @@ impl Editor { self.edit_prediction_settings = self.edit_prediction_settings_at_position(&buffer, cursor_buffer_position, cx); - if let EditPredictionSettings::Disabled = self.edit_prediction_settings { - self.discard_edit_prediction(false, cx); - return None; - }; - self.edit_prediction_indent_conflict = multibuffer.is_line_whitespace_upto(cursor); if self.edit_prediction_indent_conflict { @@ -7795,15 +7722,15 @@ impl Editor { let indents = multibuffer.suggested_indents(cursor_point.row..cursor_point.row + 1, cx); - if let Some((_, indent)) = indents.iter().next() - && indent.len == cursor_point.column - { - self.edit_prediction_indent_conflict = false; + if let Some((_, indent)) = indents.iter().next() { + if indent.len == cursor_point.column { + self.edit_prediction_indent_conflict = false; + } } } - let edit_prediction = provider.suggest(&buffer, cursor_buffer_position, cx)?; - let edits = edit_prediction + let inline_completion = provider.suggest(&buffer, cursor_buffer_position, cx)?; + let edits = inline_completion .edits .into_iter() .flat_map(|(range, new_text)| { @@ -7837,22 +7764,16 @@ impl Editor { } else { None }; - let supports_jump = self - .edit_prediction_provider - .as_ref() - .map(|provider| provider.provider.supports_jump_to_edit()) - .unwrap_or(true); - - let is_move = supports_jump - && (move_invalidation_row_range.is_some() || self.edit_predictions_hidden_for_vim_mode); + let is_move = + move_invalidation_row_range.is_some() || self.inline_completions_hidden_for_vim_mode; let completion = if is_move { invalidation_row_range = move_invalidation_row_range.unwrap_or(edit_start_row..edit_end_row); let target = first_edit_start; - EditPrediction::Move { target, snapshot } + InlineCompletion::Move { target, snapshot } } else { let show_completions_in_buffer = !self.edit_prediction_visible_in_cursor_popover(true) - && !self.edit_predictions_hidden_for_vim_mode; + && !self.inline_completions_hidden_for_vim_mode; if show_completions_in_buffer { if edits @@ -7861,7 +7782,7 @@ impl Editor { { let mut inlays = Vec::new(); for (range, new_text) in &edits { - let inlay = Inlay::edit_prediction( + let inlay = Inlay::inline_completion( post_inc(&mut self.next_inlay_id), range.start, new_text.as_str(), @@ -7873,7 +7794,7 @@ impl Editor { self.splice_inlays(&[], inlays, cx); } else { let background_color = cx.theme().status().deleted_background; - self.highlight_text::( + self.highlight_text::( edits.iter().map(|(range, _)| range.clone()).collect(), HighlightStyle { background_color: Some(background_color), @@ -7896,9 +7817,9 @@ impl Editor { EditDisplayMode::DiffPopover }; - EditPrediction::Edit { + InlineCompletion::Edit { edits, - edit_preview: edit_prediction.edit_preview, + edit_preview: inline_completion.edit_preview, display_mode, snapshot, } @@ -7911,11 +7832,11 @@ impl Editor { multibuffer.line_len(MultiBufferRow(invalidation_row_range.end)), )); - self.stale_edit_prediction_in_menu = None; - self.active_edit_prediction = Some(EditPredictionState { + self.stale_inline_completion_in_menu = None; + self.active_inline_completion = Some(InlineCompletionState { inlay_ids, completion, - completion_id: edit_prediction.id, + completion_id: inline_completion.id, invalidation_range, }); @@ -7924,7 +7845,7 @@ impl Editor { Some(()) } - pub fn edit_prediction_provider(&self) -> Option> { + pub fn edit_prediction_provider(&self) -> Option> { Some(self.edit_prediction_provider.as_ref()?.provider.clone()) } @@ -7961,7 +7882,7 @@ impl Editor { let snapshot = self.snapshot(window, cx); let multi_buffer_snapshot = &snapshot.display_snapshot.buffer_snapshot; - let Some(project) = self.project() else { + let Some(project) = self.project.as_ref() else { return breakpoint_display_points; }; @@ -7990,7 +7911,7 @@ impl Editor { let multi_buffer_anchor = Anchor::in_buffer(excerpt_id, buffer_snapshot.remote_id(), breakpoint.position); let position = multi_buffer_anchor - .to_point(multi_buffer_snapshot) + .to_point(&multi_buffer_snapshot) .to_display_point(&snapshot); breakpoint_display_points.insert( @@ -8244,6 +8165,8 @@ impl Editor { .icon_color(color) .style(ButtonStyle::Transparent) .on_click(cx.listener({ + let breakpoint = breakpoint.clone(); + move |editor, event: &ClickEvent, window, cx| { let edit_action = if event.modifiers().platform || breakpoint.is_disabled() { BreakpointEditAction::InvertState @@ -8264,7 +8187,7 @@ impl Editor { editor.set_breakpoint_context_menu( row, Some(position), - event.position(), + event.down.position, window, cx, ); @@ -8422,33 +8345,26 @@ impl Editor { let color = Color::Muted; let position = breakpoint.as_ref().map(|(anchor, _, _)| *anchor); - IconButton::new( - ("run_indicator", row.0 as usize), - ui::IconName::PlayOutlined, - ) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::XSmall) - .icon_color(color) - .toggle_state(is_active) - .on_click(cx.listener(move |editor, e: &ClickEvent, window, cx| { - let quick_launch = match e { - ClickEvent::Keyboard(_) => true, - ClickEvent::Mouse(e) => e.down.button == MouseButton::Left, - }; - - window.focus(&editor.focus_handle(cx)); - editor.toggle_code_actions( - &ToggleCodeActions { - deployed_from: Some(CodeActionSource::RunMenu(row)), - quick_launch, - }, - window, - cx, - ); - })) - .on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| { - editor.set_breakpoint_context_menu(row, position, event.position(), window, cx); - })) + IconButton::new(("run_indicator", row.0 as usize), ui::IconName::Play) + .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::XSmall) + .icon_color(color) + .toggle_state(is_active) + .on_click(cx.listener(move |editor, e: &ClickEvent, window, cx| { + let quick_launch = e.down.button == MouseButton::Left; + window.focus(&editor.focus_handle(cx)); + editor.toggle_code_actions( + &ToggleCodeActions { + deployed_from: Some(CodeActionSource::RunMenu(row)), + quick_launch, + }, + window, + cx, + ); + })) + .on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| { + editor.set_breakpoint_context_menu(row, position, event.down.position, window, cx); + })) } pub fn context_menu_visible(&self) -> bool { @@ -8457,7 +8373,7 @@ impl Editor { .context_menu .borrow() .as_ref() - .is_some_and(|menu| menu.visible()) + .map_or(false, |menu| menu.visible()) } pub fn context_menu_origin(&self) -> Option { @@ -8495,14 +8411,14 @@ impl Editor { if self.mode().is_minimap() { return None; } - let active_edit_prediction = self.active_edit_prediction.as_ref()?; + let active_inline_completion = self.active_inline_completion.as_ref()?; if self.edit_prediction_visible_in_cursor_popover(true) { return None; } - match &active_edit_prediction.completion { - EditPrediction::Move { target, .. } => { + match &active_inline_completion.completion { + InlineCompletion::Move { target, .. } => { let target_display_point = target.to_display_point(editor_snapshot); if self.edit_prediction_requires_modifier() { @@ -8539,11 +8455,11 @@ impl Editor { ) } } - EditPrediction::Edit { + InlineCompletion::Edit { display_mode: EditDisplayMode::Inline, .. } => None, - EditPrediction::Edit { + InlineCompletion::Edit { display_mode: EditDisplayMode::TabAccept, edits, .. @@ -8564,7 +8480,7 @@ impl Editor { cx, ) } - EditPrediction::Edit { + InlineCompletion::Edit { edits, edit_preview, display_mode: EditDisplayMode::DiffPopover, @@ -8880,12 +8796,8 @@ impl Editor { return None; } - let highlighted_edits = if let Some(edit_preview) = edit_preview.as_ref() { - crate::edit_prediction_edit_text(snapshot, edits, edit_preview, false, cx) - } else { - // Fallback for providers without edit_preview - crate::edit_prediction_fallback_text(edits, cx) - }; + let highlighted_edits = + crate::inline_completion_edit_text(&snapshot, edits, edit_preview.as_ref()?, false, cx); let styled_text = highlighted_edits.to_styled_text(&style.text); let line_count = highlighted_edits.text.lines().count(); @@ -9003,8 +8915,9 @@ impl Editor { let end_row = start_row + line_count as u32; visible_row_range.contains(&start_row) && visible_row_range.contains(&end_row) - && cursor_row - .is_none_or(|cursor_row| !((start_row..end_row).contains(&cursor_row))) + && cursor_row.map_or(true, |cursor_row| { + !((start_row..end_row).contains(&cursor_row)) + }) })?; content_origin @@ -9044,7 +8957,7 @@ impl Editor { let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac; - let modifiers_color = if accept_keystroke.display_modifiers == window.modifiers() { + let modifiers_color = if accept_keystroke.modifiers == window.modifiers() { Color::Accent } else { Color::Muted @@ -9056,19 +8969,19 @@ impl Editor { .font(theme::ThemeSettings::get_global(cx).buffer_font.clone()) .text_size(TextSize::XSmall.rems(cx)) .child(h_flex().children(ui::render_modifiers( - &accept_keystroke.display_modifiers, + &accept_keystroke.modifiers, PlatformStyle::platform(), Some(modifiers_color), Some(IconSize::XSmall.rems().into()), true, ))) .when(is_platform_style_mac, |parent| { - parent.child(accept_keystroke.display_key.clone()) + parent.child(accept_keystroke.key.clone()) }) .when(!is_platform_style_mac, |parent| { parent.child( Key::new( - util::capitalize(&accept_keystroke.display_key), + util::capitalize(&accept_keystroke.key), Some(Color::Default), ) .size(Some(IconSize::XSmall.rems().into())), @@ -9152,18 +9065,6 @@ impl Editor { let editor_bg_color = cx.theme().colors().editor_background; editor_bg_color.blend(accent_color.opacity(0.6)) } - fn get_prediction_provider_icon_name( - provider: &Option, - ) -> IconName { - match provider { - Some(provider) => match provider.provider.name() { - "copilot" => IconName::Copilot, - "supermaven" => IconName::Supermaven, - _ => IconName::ZedPredict, - }, - None => IconName::ZedPredict, - } - } fn render_edit_prediction_cursor_popover( &self, @@ -9171,20 +9072,62 @@ impl Editor { max_width: Pixels, cursor_point: Point, style: &EditorStyle, - accept_keystroke: Option<&gpui::KeybindingKeystroke>, + accept_keystroke: Option<&gpui::Keystroke>, _window: &Window, cx: &mut Context, ) -> Option { let provider = self.edit_prediction_provider.as_ref()?; - let provider_icon = Self::get_prediction_provider_icon_name(&self.edit_prediction_provider); + + if provider.provider.needs_terms_acceptance(cx) { + return Some( + h_flex() + .min_w(min_width) + .flex_1() + .px_2() + .py_1() + .gap_3() + .elevation_2(cx) + .hover(|style| style.bg(cx.theme().colors().element_hover)) + .id("accept-terms") + .cursor_pointer() + .on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default()) + .on_click(cx.listener(|this, _event, window, cx| { + cx.stop_propagation(); + this.report_editor_event("Edit Prediction Provider ToS Clicked", None, cx); + window.dispatch_action( + zed_actions::OpenZedPredictOnboarding.boxed_clone(), + cx, + ); + })) + .child( + h_flex() + .flex_1() + .gap_2() + .child(Icon::new(IconName::ZedPredict)) + .child(Label::new("Accept Terms of Service")) + .child(div().w_full()) + .child( + Icon::new(IconName::ArrowUpRight) + .color(Color::Muted) + .size(IconSize::Small), + ) + .into_any_element(), + ) + .into_any(), + ); + } let is_refreshing = provider.provider.is_refreshing(cx); - fn pending_completion_container(icon: IconName) -> Div { - h_flex().h_full().flex_1().gap_2().child(Icon::new(icon)) + fn pending_completion_container() -> Div { + h_flex() + .h_full() + .flex_1() + .gap_2() + .child(Icon::new(IconName::ZedPredict)) } - let completion = match &self.active_edit_prediction { + let completion = match &self.active_inline_completion { Some(prediction) => { if !self.has_visible_completions_menu() { const RADIUS: Pixels = px(6.); @@ -9202,16 +9145,16 @@ impl Editor { .rounded_tl(px(0.)) .overflow_hidden() .child(div().px_1p5().child(match &prediction.completion { - EditPrediction::Move { target, snapshot } => { + InlineCompletion::Move { target, snapshot } => { use text::ToPoint as _; - if target.text_anchor.to_point(snapshot).row > cursor_point.row + if target.text_anchor.to_point(&snapshot).row > cursor_point.row { Icon::new(IconName::ZedPredictDown) } else { Icon::new(IconName::ZedPredictUp) } } - EditPrediction::Edit { .. } => Icon::new(provider_icon), + InlineCompletion::Edit { .. } => Icon::new(IconName::ZedPredict), })) .child( h_flex() @@ -9249,7 +9192,7 @@ impl Editor { accept_keystroke.as_ref(), |el, accept_keystroke| { el.child(h_flex().children(ui::render_modifiers( - &accept_keystroke.display_modifiers, + &accept_keystroke.modifiers, PlatformStyle::platform(), Some(Color::Default), Some(IconSize::XSmall.rems().into()), @@ -9270,7 +9213,7 @@ impl Editor { )? } - None if is_refreshing => match &self.stale_edit_prediction_in_menu { + None if is_refreshing => match &self.stale_inline_completion_in_menu { Some(stale_completion) => self.render_edit_prediction_cursor_popover_preview( stale_completion, cursor_point, @@ -9278,15 +9221,15 @@ impl Editor { cx, )?, - None => pending_completion_container(provider_icon) - .child(Label::new("...").size(LabelSize::Small)), + None => { + pending_completion_container().child(Label::new("...").size(LabelSize::Small)) + } }, - None => pending_completion_container(provider_icon) - .child(Label::new("...").size(LabelSize::Small)), + None => pending_completion_container().child(Label::new("No Prediction")), }; - let completion = if is_refreshing || self.active_edit_prediction.is_none() { + let completion = if is_refreshing { completion .with_animation( "loading-completion", @@ -9300,7 +9243,7 @@ impl Editor { completion.into_any_element() }; - let has_completion = self.active_edit_prediction.is_some(); + let has_completion = self.active_inline_completion.is_some(); let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac; Some( @@ -9319,7 +9262,7 @@ impl Editor { .child(completion), ) .when_some(accept_keystroke, |el, accept_keystroke| { - if !accept_keystroke.display_modifiers.modified() { + if !accept_keystroke.modifiers.modified() { return el; } @@ -9338,7 +9281,7 @@ impl Editor { .font(theme::ThemeSettings::get_global(cx).buffer_font.clone()) .when(is_platform_style_mac, |parent| parent.gap_1()) .child(h_flex().children(ui::render_modifiers( - &accept_keystroke.display_modifiers, + &accept_keystroke.modifiers, PlatformStyle::platform(), Some(if !has_completion { Color::Muted @@ -9359,7 +9302,7 @@ impl Editor { fn render_edit_prediction_cursor_popover_preview( &self, - completion: &EditPredictionState, + completion: &InlineCompletionState, cursor_point: Point, style: &EditorStyle, cx: &mut Context, @@ -9386,51 +9329,40 @@ impl Editor { .child(Icon::new(arrow).color(Color::Muted).size(IconSize::Small)) } - let supports_jump = self - .edit_prediction_provider - .as_ref() - .map(|provider| provider.provider.supports_jump_to_edit()) - .unwrap_or(true); - match &completion.completion { - EditPrediction::Move { + InlineCompletion::Move { target, snapshot, .. - } => { - if !supports_jump { - return None; - } + } => Some( + h_flex() + .px_2() + .gap_2() + .flex_1() + .child( + if target.text_anchor.to_point(&snapshot).row > cursor_point.row { + Icon::new(IconName::ZedPredictDown) + } else { + Icon::new(IconName::ZedPredictUp) + }, + ) + .child(Label::new("Jump to Edit")), + ), - Some( - h_flex() - .px_2() - .gap_2() - .flex_1() - .child( - if target.text_anchor.to_point(snapshot).row > cursor_point.row { - Icon::new(IconName::ZedPredictDown) - } else { - Icon::new(IconName::ZedPredictUp) - }, - ) - .child(Label::new("Jump to Edit")), - ) - } - - EditPrediction::Edit { + InlineCompletion::Edit { edits, edit_preview, snapshot, display_mode: _, } => { - let first_edit_row = edits.first()?.0.start.text_anchor.to_point(snapshot).row; + let first_edit_row = edits.first()?.0.start.text_anchor.to_point(&snapshot).row; - let (highlighted_edits, has_more_lines) = - if let Some(edit_preview) = edit_preview.as_ref() { - crate::edit_prediction_edit_text(snapshot, edits, edit_preview, true, cx) - .first_line_preview() - } else { - crate::edit_prediction_fallback_text(edits, cx).first_line_preview() - }; + let (highlighted_edits, has_more_lines) = crate::inline_completion_edit_text( + &snapshot, + &edits, + edit_preview.as_ref()?, + true, + cx, + ) + .first_line_preview(); let styled_text = gpui::StyledText::new(highlighted_edits.text) .with_default_highlights(&style.text, highlighted_edits.highlights); @@ -9441,13 +9373,11 @@ impl Editor { .child(styled_text) .when(has_more_lines, |parent| parent.child("…")); - let left = if supports_jump && first_edit_row != cursor_point.row { + let left = if first_edit_row != cursor_point.row { render_relative_row_jump("", cursor_point.row, first_edit_row) .into_any_element() } else { - let icon_name = - Editor::get_prediction_provider_icon_name(&self.edit_prediction_provider); - Icon::new(icon_name).into_any_element() + Icon::new(IconName::ZedPredict).into_any_element() }; Some( @@ -9503,12 +9433,12 @@ impl Editor { cx.notify(); self.completion_tasks.clear(); let context_menu = self.context_menu.borrow_mut().take(); - self.stale_edit_prediction_in_menu.take(); - self.update_visible_edit_prediction(window, cx); - if let Some(CodeContextMenu::Completions(_)) = &context_menu - && let Some(completion_provider) = &self.completion_provider - { - completion_provider.selection_changed(None, window, cx); + self.stale_inline_completion_in_menu.take(); + self.update_visible_inline_completion(window, cx); + if let Some(CodeContextMenu::Completions(_)) = &context_menu { + if let Some(completion_provider) = &self.completion_provider { + completion_provider.selection_changed(None, window, cx); + } } context_menu } @@ -9519,21 +9449,17 @@ impl Editor { selection: Range, cx: &mut Context, ) { - let Some((_, buffer, _)) = self - .buffer() - .read(cx) - .excerpt_containing(selection.start, cx) - else { + let buffer_id = match (&selection.start.buffer_id, &selection.end.buffer_id) { + (Some(a), Some(b)) if a == b => a, + _ => { + log::error!("expected anchor range to have matching buffer IDs"); + return; + } + }; + let multi_buffer = self.buffer().read(cx); + let Some(buffer) = multi_buffer.buffer(*buffer_id) else { return; }; - let Some((_, end_buffer, _)) = self.buffer().read(cx).excerpt_containing(selection.end, cx) - else { - return; - }; - if buffer != end_buffer { - log::error!("expected anchor range to have matching buffer IDs"); - return; - } let id = post_inc(&mut self.next_completion_id); let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order; @@ -9579,7 +9505,7 @@ impl Editor { .tabstops .iter() .map(|tabstop| { - let is_end_tabstop = tabstop.ranges.first().is_some_and(|tabstop| { + let is_end_tabstop = tabstop.ranges.first().map_or(false, |tabstop| { tabstop.is_empty() && tabstop.start == snippet.text.len() as isize }); let mut tabstop_ranges = tabstop @@ -9617,10 +9543,10 @@ impl Editor { s.select_ranges(tabstop.ranges.iter().rev().cloned()); }); - if let Some(choices) = &tabstop.choices - && let Some(selection) = tabstop.ranges.first() - { - self.show_snippet_choices(choices, selection.clone(), cx) + if let Some(choices) = &tabstop.choices { + if let Some(selection) = tabstop.ranges.first() { + self.show_snippet_choices(choices, selection.clone(), cx) + } } // If we're already at the last tabstop and it's at the end of the snippet, @@ -9754,10 +9680,10 @@ impl Editor { s.select_ranges(current_ranges.iter().rev().cloned()) }); - if let Some(choices) = &snippet.choices[snippet.active_index] - && let Some(selection) = current_ranges.first() - { - self.show_snippet_choices(choices, selection.clone(), cx); + if let Some(choices) = &snippet.choices[snippet.active_index] { + if let Some(selection) = current_ranges.first() { + self.show_snippet_choices(&choices, selection.clone(), cx); + } } // If snippet state is not at the last tabstop, push it back on the stack @@ -9779,9 +9705,6 @@ impl Editor { } pub fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context) { - if self.read_only(cx) { - return; - } self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.transact(window, cx, |this, window, cx| { this.select_autoclose_pair(window, cx); @@ -9869,15 +9792,12 @@ impl Editor { this.edit(edits, None, cx); }) } - this.refresh_edit_prediction(true, false, window, cx); + this.refresh_inline_completion(true, false, window, cx); linked_editing_ranges::refresh_linked_ranges(this, window, cx); }); } pub fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context) { - if self.read_only(cx) { - return; - } self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.transact(window, cx, |this, window, cx| { this.change_selections(Default::default(), window, cx, |s| { @@ -9891,7 +9811,7 @@ impl Editor { }) }); this.insert("", window, cx); - this.refresh_edit_prediction(true, false, window, cx); + this.refresh_inline_completion(true, false, window, cx); }); } @@ -10024,7 +9944,7 @@ impl Editor { self.transact(window, cx, |this, window, cx| { this.buffer.update(cx, |b, cx| b.edit(edits, None, cx)); this.change_selections(Default::default(), window, cx, |s| s.select(selections)); - this.refresh_edit_prediction(true, false, window, cx); + this.refresh_inline_completion(true, false, window, cx); }); } @@ -10160,10 +10080,10 @@ impl Editor { // Avoid re-outdenting a row that has already been outdented by a // previous selection. - if let Some(last_row) = last_outdent - && last_row == rows.start - { - rows.start = rows.start.next_row(); + if let Some(last_row) = last_outdent { + if last_row == rows.start { + rows.start = rows.start.next_row(); + } } let has_multiple_rows = rows.len() > 1; for row in rows.iter_rows() { @@ -10341,11 +10261,11 @@ impl Editor { MultiBufferRow(selection.end.row) }; - if let Some(last_row_range) = row_ranges.last_mut() - && start <= last_row_range.end - { - last_row_range.end = end; - continue; + if let Some(last_row_range) = row_ranges.last_mut() { + if start <= last_row_range.end { + last_row_range.end = end; + continue; + } } row_ranges.push(start..end); } @@ -10523,7 +10443,7 @@ impl Editor { ) { if let Some(working_directory) = self.active_excerpt(cx).and_then(|(_, buffer, _)| { let project_path = buffer.read(cx).project_path(cx)?; - let project = self.project()?.read(cx); + let project = self.project.as_ref()?.read(cx); let entry = project.entry_for_path(&project_path, cx)?; let parent = match &entry.canonical_path { Some(canonical_path) => canonical_path.to_path_buf(), @@ -10626,12 +10546,16 @@ impl Editor { snapshot: &EditorSnapshot, cx: &mut Context, ) -> Option<(Anchor, Breakpoint)> { - let buffer = self - .buffer - .read(cx) - .buffer_for_anchor(breakpoint_position, cx)?; + let project = self.project.clone()?; + + let buffer_id = breakpoint_position.buffer_id.or_else(|| { + snapshot + .buffer_snapshot + .buffer_id_for_excerpt(breakpoint_position.excerpt_id) + })?; let enclosing_excerpt = breakpoint_position.excerpt_id; + let buffer = project.read(cx).buffer_for_id(buffer_id, cx)?; let buffer_snapshot = buffer.read(cx).snapshot(); let row = buffer_snapshot @@ -10643,7 +10567,8 @@ impl Editor { .buffer_snapshot .anchor_after(Point::new(row, line_len)); - self.breakpoint_store + let bp = self + .breakpoint_store .as_ref()? .read_with(cx, |breakpoint_store, cx| { breakpoint_store @@ -10668,7 +10593,8 @@ impl Editor { None } }) - }) + }); + bp } pub fn edit_log_breakpoint( @@ -10704,7 +10630,7 @@ impl Editor { let cursors = self .selections .disjoint_anchors() - .iter() + .into_iter() .map(|selection| { let cursor_position: Point = selection.head().to_point(&snapshot.buffer_snapshot); @@ -10804,11 +10730,21 @@ impl Editor { return; }; - let Some(buffer) = self - .buffer - .read(cx) - .buffer_for_anchor(breakpoint_position, cx) - else { + let Some(buffer_id) = breakpoint_position.buffer_id.or_else(|| { + if breakpoint_position == Anchor::min() { + self.buffer() + .read(cx) + .excerpt_buffer_ids() + .into_iter() + .next() + } else { + None + } + }) else { + return; + }; + + let Some(buffer) = self.buffer().read(cx).buffer(buffer_id) else { return; }; @@ -11036,7 +10972,7 @@ impl Editor { let mut col = 0; let mut changed = false; - for ch in chars.by_ref() { + while let Some(ch) = chars.next() { match ch { ' ' => { reindented_line.push(' '); @@ -11092,7 +11028,7 @@ impl Editor { let mut first_non_indent_char = None; let mut changed = false; - for ch in chars.by_ref() { + while let Some(ch) = chars.next() { match ch { ' ' => { // Keep track of spaces. Append \t when we reach tab_size @@ -11700,7 +11636,7 @@ impl Editor { let transpose_start = display_map .buffer_snapshot .clip_offset(transpose_offset.saturating_sub(1), Bias::Left); - if edits.last().is_none_or(|e| e.0.end <= transpose_start) { + if edits.last().map_or(true, |e| e.0.end <= transpose_start) { let transpose_end = display_map .buffer_snapshot .clip_offset(transpose_offset + 1, Bias::Right); @@ -12182,8 +12118,6 @@ impl Editor { let clipboard_text = Cow::Borrowed(text); self.transact(window, cx, |this, window, cx| { - let had_active_edit_prediction = this.has_active_edit_prediction(); - if let Some(mut clipboard_selections) = clipboard_selections { let old_selections = this.selections.all::(cx); let all_selections_were_entire_line = @@ -12256,11 +12190,6 @@ impl Editor { } else { this.insert(&clipboard_text, window, cx); } - - let trigger_in_words = - this.show_edit_predictions_in_menu() || !had_active_edit_prediction; - - this.trigger_completion_on_input(text, trigger_in_words, window, cx); }); } @@ -12344,7 +12273,7 @@ impl Editor { } self.request_autoscroll(Autoscroll::fit(), cx); self.unmark_text(window, cx); - self.refresh_edit_prediction(true, false, window, cx); + self.refresh_inline_completion(true, false, window, cx); cx.emit(EditorEvent::Edited { transaction_id }); cx.emit(EditorEvent::TransactionUndone { transaction_id }); } @@ -12374,7 +12303,7 @@ impl Editor { } self.request_autoscroll(Autoscroll::fit(), cx); self.unmark_text(window, cx); - self.refresh_edit_prediction(true, false, window, cx); + self.refresh_inline_completion(true, false, window, cx); cx.emit(EditorEvent::Edited { transaction_id }); } } @@ -12614,7 +12543,7 @@ impl Editor { return; } - if matches!(self.mode, EditorMode::SingleLine) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { cx.propagate(); return; } @@ -12738,7 +12667,7 @@ impl Editor { return; } - if matches!(self.mode, EditorMode::SingleLine) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { cx.propagate(); return; } @@ -13222,7 +13151,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { cx.propagate(); return; } @@ -13243,7 +13172,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { cx.propagate(); return; } @@ -13264,7 +13193,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { cx.propagate(); return; } @@ -13285,7 +13214,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { cx.propagate(); return; } @@ -13306,7 +13235,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { cx.propagate(); return; } @@ -13331,7 +13260,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { cx.propagate(); return; } @@ -13356,7 +13285,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { cx.propagate(); return; } @@ -13381,7 +13310,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { cx.propagate(); return; } @@ -13406,7 +13335,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { cx.propagate(); return; } @@ -13427,7 +13356,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { cx.propagate(); return; } @@ -13448,7 +13377,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { cx.propagate(); return; } @@ -13469,7 +13398,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { cx.propagate(); return; } @@ -13490,7 +13419,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { cx.propagate(); return; } @@ -13515,7 +13444,7 @@ impl Editor { } pub fn move_to_end(&mut self, _: &MoveToEnd, window: &mut Window, cx: &mut Context) { - if matches!(self.mode, EditorMode::SingleLine) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { cx.propagate(); return; } @@ -13618,7 +13547,7 @@ impl Editor { pub fn split_selection_into_lines( &mut self, - action: &SplitSelectionIntoLines, + _: &SplitSelectionIntoLines, window: &mut Window, cx: &mut Context, ) { @@ -13635,21 +13564,8 @@ impl Editor { let buffer = self.buffer.read(cx).read(cx); for selection in selections { for row in selection.start.row..selection.end.row { - let line_start = Point::new(row, 0); - let line_end = Point::new(row, buffer.line_len(MultiBufferRow(row))); - - if action.keep_selections { - // Keep the selection range for each line - let selection_start = if row == selection.start.row { - selection.start - } else { - line_start - }; - new_selection_ranges.push(selection_start..line_end); - } else { - // Collapse to cursor at end of line - new_selection_ranges.push(line_end..line_end); - } + let cursor = Point::new(row, buffer.line_len(MultiBufferRow(row))); + new_selection_ranges.push(cursor..cursor); } let is_multiline_selection = selection.start.row != selection.end.row; @@ -13657,16 +13573,7 @@ impl Editor { // so this action feels more ergonomic when paired with other selection operations let should_skip_last = is_multiline_selection && selection.end.column == 0; if !should_skip_last { - if action.keep_selections { - if is_multiline_selection { - let line_start = Point::new(selection.end.row, 0); - new_selection_ranges.push(line_start..selection.end); - } else { - new_selection_ranges.push(selection.start..selection.end); - } - } else { - new_selection_ranges.push(selection.end..selection.end); - } + new_selection_ranges.push(selection.end..selection.end); } } } @@ -14564,7 +14471,7 @@ impl Editor { let advance_downwards = action.advance_downwards && selections_on_single_row && !selections_selecting - && !matches!(this.mode, EditorMode::SingleLine); + && !matches!(this.mode, EditorMode::SingleLine { .. }); if advance_downwards { let snapshot = this.buffer.read(cx).snapshot(cx); @@ -14801,94 +14708,12 @@ impl Editor { } } - pub fn unwrap_syntax_node( - &mut self, - _: &UnwrapSyntaxNode, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); - - let buffer = self.buffer.read(cx).snapshot(cx); - let selections = self - .selections - .all::(cx) - .into_iter() - // subtracting the offset requires sorting - .sorted_by_key(|i| i.start); - - let full_edits = selections - .into_iter() - .filter_map(|selection| { - // Only requires two branches once if-let-chains stabilize (#53667) - let child = if !selection.is_empty() { - selection.range() - } else if let Some((_, ancestor_range)) = - buffer.syntax_ancestor(selection.start..selection.end) - { - match ancestor_range { - MultiOrSingleBufferOffsetRange::Single(range) => range, - MultiOrSingleBufferOffsetRange::Multi(range) => range, - } - } else { - selection.range() - }; - - let mut parent = child.clone(); - while let Some((_, ancestor_range)) = buffer.syntax_ancestor(parent.clone()) { - parent = match ancestor_range { - MultiOrSingleBufferOffsetRange::Single(range) => range, - MultiOrSingleBufferOffsetRange::Multi(range) => range, - }; - if parent.start < child.start || parent.end > child.end { - break; - } - } - - if parent == child { - return None; - } - let text = buffer.text_for_range(child).collect::(); - Some((selection.id, parent, text)) - }) - .collect::>(); - - self.transact(window, cx, |this, window, cx| { - this.buffer.update(cx, |buffer, cx| { - buffer.edit( - full_edits - .iter() - .map(|(_, p, t)| (p.clone(), t.clone())) - .collect::>(), - None, - cx, - ); - }); - this.change_selections(Default::default(), window, cx, |s| { - let mut offset = 0; - let mut selections = vec![]; - for (id, parent, text) in full_edits { - let start = parent.start - offset; - offset += parent.len() - text.len(); - selections.push(Selection { - id, - start, - end: start + text.len(), - reversed: false, - goal: Default::default(), - }); - } - s.select(selections); - }); - }); - } - fn refresh_runnables(&mut self, window: &mut Window, cx: &mut Context) -> Task<()> { if !EditorSettings::get_global(cx).gutter.runnables { self.clear_tasks(); return Task::ready(()); } - let project = self.project().map(Entity::downgrade); + let project = self.project.as_ref().map(Entity::downgrade); let task_sources = self.lsp_task_sources(cx); let multi_buffer = self.buffer.downgrade(); cx.spawn_in(window, async move |editor, cx| { @@ -14903,7 +14728,10 @@ impl Editor { }; let hide_runnables = project - .update(cx, |project, _| project.is_via_collab()) + .update(cx, |project, cx| { + // Do not display any test indicators in non-dev server remote projects. + project.is_via_collab() && project.ssh_connection_string(cx).is_none() + }) .unwrap_or(true); if hide_runnables { return; @@ -15296,15 +15124,17 @@ impl Editor { if direction == ExpandExcerptDirection::Down { let multi_buffer = self.buffer.read(cx); let snapshot = multi_buffer.snapshot(cx); - if let Some(buffer_id) = snapshot.buffer_id_for_excerpt(excerpt) - && let Some(buffer) = multi_buffer.buffer(buffer_id) - && let Some(excerpt_range) = snapshot.buffer_range_for_excerpt(excerpt) - { - let buffer_snapshot = buffer.read(cx).snapshot(); - let excerpt_end_row = Point::from_anchor(&excerpt_range.end, &buffer_snapshot).row; - let last_row = buffer_snapshot.max_point().row; - let lines_below = last_row.saturating_sub(excerpt_end_row); - should_scroll_up = lines_below >= lines_to_expand; + if let Some(buffer_id) = snapshot.buffer_id_for_excerpt(excerpt) { + if let Some(buffer) = multi_buffer.buffer(buffer_id) { + if let Some(excerpt_range) = snapshot.buffer_range_for_excerpt(excerpt) { + let buffer_snapshot = buffer.read(cx).snapshot(); + let excerpt_end_row = + Point::from_anchor(&excerpt_range.end, &buffer_snapshot).row; + let last_row = buffer_snapshot.max_point().row; + let lines_below = last_row.saturating_sub(excerpt_end_row); + should_scroll_up = lines_below >= lines_to_expand; + } + } } } @@ -15389,10 +15219,10 @@ impl Editor { let selection = self.selections.newest::(cx); let mut active_group_id = None; - if let ActiveDiagnostic::Group(active_group) = &self.active_diagnostics - && active_group.active_range.start.to_offset(&buffer) == selection.start - { - active_group_id = Some(active_group.group_id); + if let ActiveDiagnostic::Group(active_group) = &self.active_diagnostics { + if active_group.active_range.start.to_offset(&buffer) == selection.start { + active_group_id = Some(active_group.group_id); + } } fn filtered( @@ -15451,8 +15281,7 @@ impl Editor { return; }; - let next_diagnostic_start = buffer.anchor_after(next_diagnostic.range.start); - let Some(buffer_id) = buffer.buffer_id_for_anchor(next_diagnostic_start) else { + let Some(buffer_id) = buffer.anchor_after(next_diagnostic.range.start).buffer_id else { return; }; self.change_selections(Default::default(), window, cx, |s| { @@ -15461,7 +15290,7 @@ impl Editor { ]) }); self.activate_diagnostics(buffer_id, next_diagnostic, window, cx); - self.refresh_edit_prediction(false, true, window, cx); + self.refresh_inline_completion(false, true, window, cx); } pub fn go_to_next_hunk(&mut self, _: &GoToHunk, window: &mut Window, cx: &mut Context) { @@ -15722,17 +15551,18 @@ impl Editor { }; let head = self.selections.newest::(cx).head(); let buffer = self.buffer.read(cx); - let Some((buffer, head)) = buffer.text_anchor_for_position(head, cx) else { + let (buffer, head) = if let Some(text_anchor) = buffer.text_anchor_for_position(head, cx) { + text_anchor + } else { return Task::ready(Ok(Navigated::No)); }; + let Some(definitions) = provider.definitions(&buffer, head, kind, cx) else { return Task::ready(Ok(Navigated::No)); }; cx.spawn_in(window, async move |editor, cx| { - let Some(definitions) = definitions.await? else { - return Ok(Navigated::No); - }; + let definitions = definitions.await?; let navigated = editor .update_in(cx, |editor, window, cx| { editor.navigate_to_hover_links( @@ -15831,120 +15661,62 @@ impl Editor { pub(crate) fn navigate_to_hover_links( &mut self, kind: Option, - definitions: Vec, + mut definitions: Vec, split: bool, window: &mut Window, cx: &mut Context, ) -> Task> { - // Separate out url and file links, we can only handle one of them at most or an arbitrary number of locations - let mut first_url_or_file = None; - let definitions: Vec<_> = definitions - .into_iter() - .filter_map(|def| match def { - HoverLink::Text(link) => Some(Task::ready(anyhow::Ok(Some(link.target)))), + // If there is one definition, just open it directly + if definitions.len() == 1 { + let definition = definitions.pop().unwrap(); + + enum TargetTaskResult { + Location(Option), + AlreadyNavigated, + } + + let target_task = match definition { + HoverLink::Text(link) => { + Task::ready(anyhow::Ok(TargetTaskResult::Location(Some(link.target)))) + } HoverLink::InlayHint(lsp_location, server_id) => { let computation = self.compute_target_location(lsp_location, server_id, window, cx); - Some(cx.background_spawn(computation)) + cx.background_spawn(async move { + let location = computation.await?; + Ok(TargetTaskResult::Location(location)) + }) } HoverLink::Url(url) => { - first_url_or_file = Some(Either::Left(url)); - None + cx.open_url(&url); + Task::ready(Ok(TargetTaskResult::AlreadyNavigated)) } HoverLink::File(path) => { - first_url_or_file = Some(Either::Right(path)); - None - } - }) - .collect(); - - let workspace = self.workspace(); - - cx.spawn_in(window, async move |editor, acx| { - let mut locations: Vec = future::join_all(definitions) - .await - .into_iter() - .filter_map(|location| location.transpose()) - .collect::>() - .context("location tasks")?; - - if locations.len() > 1 { - let Some(workspace) = workspace else { - return Ok(Navigated::No); - }; - - let tab_kind = match kind { - Some(GotoDefinitionKind::Implementation) => "Implementations", - Some(GotoDefinitionKind::Symbol) | None => "Definitions", - Some(GotoDefinitionKind::Declaration) => "Declarations", - Some(GotoDefinitionKind::Type) => "Types", - }; - let title = editor - .update_in(acx, |_, _, cx| { - let target = locations - .iter() - .map(|location| { - location - .buffer - .read(cx) - .text_for_range(location.range.clone()) - .collect::() - }) - .filter(|text| !text.contains('\n')) - .unique() - .take(3) - .join(", "); - if target.is_empty() { - tab_kind.to_owned() - } else { - format!("{tab_kind} for {target}") - } - }) - .context("buffer title")?; - - let opened = workspace - .update_in(acx, |workspace, window, cx| { - Self::open_locations_in_multibuffer( - workspace, - locations, - title, - split, - MultibufferSelectionMode::First, - window, - cx, - ) - }) - .is_ok(); - - anyhow::Ok(Navigated::from_bool(opened)) - } else if locations.is_empty() { - // If there is one definition, just open it directly - match first_url_or_file { - Some(Either::Left(url)) => { - acx.update(|_, cx| cx.open_url(&url))?; - Ok(Navigated::Yes) + if let Some(workspace) = self.workspace() { + cx.spawn_in(window, async move |_, cx| { + workspace + .update_in(cx, |workspace, window, cx| { + workspace.open_resolved_path(path, window, cx) + })? + .await + .map(|_| TargetTaskResult::AlreadyNavigated) + }) + } else { + Task::ready(Ok(TargetTaskResult::Location(None))) } - Some(Either::Right(path)) => { - let Some(workspace) = workspace else { - return Ok(Navigated::No); - }; - - workspace - .update_in(acx, |workspace, window, cx| { - workspace.open_resolved_path(path, window, cx) - })? - .await?; - Ok(Navigated::Yes) - } - None => Ok(Navigated::No), } - } else { - let Some(workspace) = workspace else { - return Ok(Navigated::No); + }; + cx.spawn_in(window, async move |editor, cx| { + let target = match target_task.await.context("target resolution task")? { + TargetTaskResult::AlreadyNavigated => return Ok(Navigated::Yes), + TargetTaskResult::Location(None) => return Ok(Navigated::No), + TargetTaskResult::Location(Some(target)) => target, }; - let target = locations.pop().unwrap(); - editor.update_in(acx, |editor, window, cx| { + editor.update_in(cx, |editor, window, cx| { + let Some(workspace) = editor.workspace() else { + return Navigated::No; + }; let pane = workspace.read(cx).active_pane().clone(); let range = target.range.to_point(target.buffer.read(cx)); @@ -15954,7 +15726,7 @@ impl Editor { if !split && Some(&target.buffer) == editor.buffer.read(cx).as_singleton().as_ref() { - editor.go_to_singleton_buffer_range(range, window, cx); + editor.go_to_singleton_buffer_range(range.clone(), window, cx); } else { window.defer(cx, move |window, cx| { let target_editor: Entity = @@ -15985,8 +15757,81 @@ impl Editor { } Navigated::Yes }) - } - }) + }) + } else if !definitions.is_empty() { + cx.spawn_in(window, async move |editor, cx| { + let (title, location_tasks, workspace) = editor + .update_in(cx, |editor, window, cx| { + let tab_kind = match kind { + Some(GotoDefinitionKind::Implementation) => "Implementations", + _ => "Definitions", + }; + let title = definitions + .iter() + .find_map(|definition| match definition { + HoverLink::Text(link) => link.origin.as_ref().map(|origin| { + let buffer = origin.buffer.read(cx); + format!( + "{} for {}", + tab_kind, + buffer + .text_for_range(origin.range.clone()) + .collect::() + ) + }), + HoverLink::InlayHint(_, _) => None, + HoverLink::Url(_) => None, + HoverLink::File(_) => None, + }) + .unwrap_or(tab_kind.to_string()); + let location_tasks = definitions + .into_iter() + .map(|definition| match definition { + HoverLink::Text(link) => Task::ready(Ok(Some(link.target))), + HoverLink::InlayHint(lsp_location, server_id) => editor + .compute_target_location(lsp_location, server_id, window, cx), + HoverLink::Url(_) => Task::ready(Ok(None)), + HoverLink::File(_) => Task::ready(Ok(None)), + }) + .collect::>(); + (title, location_tasks, editor.workspace().clone()) + }) + .context("location tasks preparation")?; + + let locations: Vec = future::join_all(location_tasks) + .await + .into_iter() + .filter_map(|location| location.transpose()) + .collect::>() + .context("location tasks")?; + + if locations.is_empty() { + return Ok(Navigated::No); + } + + let Some(workspace) = workspace else { + return Ok(Navigated::No); + }; + + let opened = workspace + .update_in(cx, |workspace, window, cx| { + Self::open_locations_in_multibuffer( + workspace, + locations, + title, + split, + MultibufferSelectionMode::First, + window, + cx, + ) + }) + .ok(); + + anyhow::Ok(Navigated::from_bool(opened.is_some())) + }) + } else { + Task::ready(Ok(Navigated::No)) + } } fn compute_target_location( @@ -16003,24 +15848,38 @@ impl Editor { cx.spawn_in(window, async move |editor, cx| { let location_task = editor.update(cx, |_, cx| { project.update(cx, |project, cx| { - project.open_local_buffer_via_lsp(lsp_location.uri.clone(), server_id, cx) + let language_server_name = project + .language_server_statuses(cx) + .find(|(id, _)| server_id == *id) + .map(|(_, status)| LanguageServerName::from(status.name.as_str())); + language_server_name.map(|language_server_name| { + project.open_local_buffer_via_lsp( + lsp_location.uri.clone(), + server_id, + language_server_name, + cx, + ) + }) }) })?; - let location = Some({ - let target_buffer_handle = location_task.await.context("open local buffer")?; - let range = target_buffer_handle.read_with(cx, |target_buffer, _| { - let target_start = target_buffer - .clip_point_utf16(point_from_lsp(lsp_location.range.start), Bias::Left); - let target_end = target_buffer - .clip_point_utf16(point_from_lsp(lsp_location.range.end), Bias::Left); - target_buffer.anchor_after(target_start) - ..target_buffer.anchor_before(target_end) - })?; - Location { - buffer: target_buffer_handle, - range, - } - }); + let location = match location_task { + Some(task) => Some({ + let target_buffer_handle = task.await.context("open local buffer")?; + let range = target_buffer_handle.read_with(cx, |target_buffer, _| { + let target_start = target_buffer + .clip_point_utf16(point_from_lsp(lsp_location.range.start), Bias::Left); + let target_end = target_buffer + .clip_point_utf16(point_from_lsp(lsp_location.range.end), Bias::Left); + target_buffer.anchor_after(target_start) + ..target_buffer.anchor_before(target_end) + })?; + Location { + buffer: target_buffer_handle, + range, + } + }), + None => None, + }; Ok(location) }) } @@ -16074,32 +15933,25 @@ impl Editor { } }); - let Some(locations) = references.await? else { - return anyhow::Ok(Navigated::No); - }; + let locations = references.await?; if locations.is_empty() { return anyhow::Ok(Navigated::No); } workspace.update_in(cx, |workspace, window, cx| { - let target = locations - .iter() + let title = locations + .first() + .as_ref() .map(|location| { - location - .buffer - .read(cx) - .text_for_range(location.range.clone()) - .collect::() + let buffer = location.buffer.read(cx); + format!( + "References to `{}`", + buffer + .text_for_range(location.range.clone()) + .collect::() + ) }) - .filter(|text| !text.contains('\n')) - .unique() - .take(3) - .join(", "); - let title = if target.is_empty() { - "References".to_owned() - } else { - format!("References to {target}") - }; + .unwrap(); Self::open_locations_in_multibuffer( workspace, locations, @@ -16214,22 +16066,24 @@ impl Editor { let item_id = item.item_id(); if split { - workspace.split_item(SplitDirection::Right, item, window, cx); - } else if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation { - let (preview_item_id, preview_item_idx) = - workspace.active_pane().read_with(cx, |pane, _| { - (pane.preview_item_id(), pane.preview_item_idx()) - }); - - workspace.add_item_to_active_pane(item, preview_item_idx, true, window, cx); - - if let Some(preview_item_id) = preview_item_id { - workspace.active_pane().update(cx, |pane, cx| { - pane.remove_item(preview_item_id, false, false, window, cx); - }); - } + workspace.split_item(SplitDirection::Right, item.clone(), window, cx); } else { - workspace.add_item_to_active_pane(item, None, true, window, cx); + if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation { + let (preview_item_id, preview_item_idx) = + workspace.active_pane().read_with(cx, |pane, _| { + (pane.preview_item_id(), pane.preview_item_idx()) + }); + + workspace.add_item_to_active_pane(item.clone(), preview_item_idx, true, window, cx); + + if let Some(preview_item_id) = preview_item_id { + workspace.active_pane().update(cx, |pane, cx| { + pane.remove_item(preview_item_id, false, false, window, cx); + }); + } + } else { + workspace.add_item_to_active_pane(item.clone(), None, true, window, cx); + } } workspace.active_pane().update(cx, |pane, cx| { pane.set_preview_item_id(Some(item_id), cx); @@ -16400,7 +16254,7 @@ impl Editor { font_weight: Some(FontWeight::BOLD), ..make_inlay_hints_style(cx.app) }, - edit_prediction_styles: make_suggestion_styles( + inline_completion_styles: make_suggestion_styles( cx.app, ), ..EditorStyle::default() @@ -16619,7 +16473,10 @@ impl Editor { .transaction(transaction_id_prev) .map(|t| t.0.clone()) }) - .unwrap_or_else(|| self.selections.disjoint_anchors()); + .unwrap_or_else(|| { + log::info!("Failed to determine selections from before format. Falling back to selections when format was initiated"); + self.selections.disjoint_anchors() + }); let mut timeout = cx.background_executor().timer(FORMAT_TIMEOUT).fuse(); let format = project.update(cx, |project, cx| { @@ -16637,10 +16494,10 @@ impl Editor { buffer .update(cx, |buffer, cx| { - if let Some(transaction) = transaction - && !buffer.is_singleton() - { - buffer.push_transaction(&transaction.0, cx); + if let Some(transaction) = transaction { + if !buffer.is_singleton() { + buffer.push_transaction(&transaction.0, cx); + } } cx.notify(); }) @@ -16706,10 +16563,10 @@ impl Editor { buffer .update(cx, |buffer, cx| { // check if we need this - if let Some(transaction) = transaction - && !buffer.is_singleton() - { - buffer.push_transaction(&transaction.0, cx); + if let Some(transaction) = transaction { + if !buffer.is_singleton() { + buffer.push_transaction(&transaction.0, cx); + } } cx.notify(); }) @@ -17058,7 +16915,7 @@ impl Editor { if !pull_diagnostics_settings.enabled { return None; } - let project = self.project()?.downgrade(); + let project = self.project.as_ref()?.downgrade(); let debounce = Duration::from_millis(pull_diagnostics_settings.debounce_ms); let mut buffers = self.buffer.read(cx).all_buffers(); if let Some(buffer_id) = buffer_id { @@ -17341,12 +17198,12 @@ impl Editor { } for row in (0..=range.start.row).rev() { - if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) - && crease.range().end.row >= buffer_start_row - { - to_fold.push(crease); - if row <= range.start.row { - break; + if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) { + if crease.range().end.row >= buffer_start_row { + to_fold.push(crease); + if row <= range.start.row { + break; + } } } } @@ -17874,7 +17731,7 @@ impl Editor { ranges: &[Range], snapshot: &MultiBufferSnapshot, ) -> bool { - let mut hunks = self.diff_hunks_in_ranges(ranges, snapshot); + let mut hunks = self.diff_hunks_in_ranges(ranges, &snapshot); hunks.any(|hunk| hunk.status().has_secondary_hunk()) } @@ -18022,7 +17879,7 @@ impl Editor { hunks: impl Iterator, cx: &mut App, ) -> Option<()> { - let project = self.project()?; + let project = self.project.as_ref()?; let buffer = project.read(cx).buffer_for_id(buffer_id, cx)?; let diff = self.buffer.read(cx).diff_for(buffer_id)?; let buffer_snapshot = buffer.read(cx).snapshot(); @@ -18656,10 +18513,10 @@ impl Editor { pub fn working_directory(&self, cx: &App) -> Option { if let Some(buffer) = self.buffer().read(cx).as_singleton() { - if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) - && let Some(dir) = file.abs_path(cx).parent() - { - return Some(dir.to_owned()); + if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) { + if let Some(dir) = file.abs_path(cx).parent() { + return Some(dir.to_owned()); + } } if let Some(project_path) = buffer.read(cx).project_path(cx) { @@ -18682,7 +18539,7 @@ impl Editor { self.active_excerpt(cx).and_then(|(_, buffer, _)| { let buffer = buffer.read(cx); if let Some(project_path) = buffer.project_path(cx) { - let project = self.project()?.read(cx); + let project = self.project.as_ref()?.read(cx); project.absolute_path(&project_path, cx) } else { buffer @@ -18695,7 +18552,7 @@ impl Editor { fn target_file_path(&self, cx: &mut Context) -> Option { self.active_excerpt(cx).and_then(|(_, buffer, _)| { let project_path = buffer.read(cx).project_path(cx)?; - let project = self.project()?.read(cx); + let project = self.project.as_ref()?.read(cx); let entry = project.entry_for_path(&project_path, cx)?; let path = entry.path.to_path_buf(); Some(path) @@ -18719,10 +18576,10 @@ impl Editor { _window: &mut Window, cx: &mut Context, ) { - if let Some(path) = self.target_file_abs_path(cx) - && let Some(path) = path.to_str() - { - cx.write_to_clipboard(ClipboardItem::new_string(path.to_string())); + if let Some(path) = self.target_file_abs_path(cx) { + if let Some(path) = path.to_str() { + cx.write_to_clipboard(ClipboardItem::new_string(path.to_string())); + } } } @@ -18732,10 +18589,10 @@ impl Editor { _window: &mut Window, cx: &mut Context, ) { - if let Some(path) = self.target_file_path(cx) - && let Some(path) = path.to_str() - { - cx.write_to_clipboard(ClipboardItem::new_string(path.to_string())); + if let Some(path) = self.target_file_path(cx) { + if let Some(path) = path.to_str() { + cx.write_to_clipboard(ClipboardItem::new_string(path.to_string())); + } } } @@ -18804,20 +18661,22 @@ impl Editor { _: &mut Window, cx: &mut Context, ) { - if let Some(file) = self.target_file(cx) - && let Some(file_stem) = file.path().file_stem() - && let Some(name) = file_stem.to_str() - { - cx.write_to_clipboard(ClipboardItem::new_string(name.to_string())); + if let Some(file) = self.target_file(cx) { + if let Some(file_stem) = file.path().file_stem() { + if let Some(name) = file_stem.to_str() { + cx.write_to_clipboard(ClipboardItem::new_string(name.to_string())); + } + } } } pub fn copy_file_name(&mut self, _: &CopyFileName, _: &mut Window, cx: &mut Context) { - if let Some(file) = self.target_file(cx) - && let Some(file_name) = file.path().file_name() - && let Some(name) = file_name.to_str() - { - cx.write_to_clipboard(ClipboardItem::new_string(name.to_string())); + if let Some(file) = self.target_file(cx) { + if let Some(file_name) = file.path().file_name() { + if let Some(name) = file_name.to_str() { + cx.write_to_clipboard(ClipboardItem::new_string(name.to_string())); + } + } } } @@ -18914,7 +18773,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if let Some(project) = self.project() { + if let Some(project) = self.project.as_ref() { let Some(buffer) = self.buffer().read(cx).as_singleton() else { return; }; @@ -18991,7 +18850,7 @@ impl Editor { fn has_blame_entries(&self, cx: &App) -> bool { self.blame() - .is_some_and(|blame| blame.read(cx).has_generated_entries()) + .map_or(false, |blame| blame.read(cx).has_generated_entries()) } fn newest_selection_head_on_empty_line(&self, cx: &App) -> bool { @@ -19018,16 +18877,19 @@ impl Editor { buffer_ranges.last() }?; - let selection = text::ToPoint::to_point(&range.start, buffer).row - ..text::ToPoint::to_point(&range.end, buffer).row; - Some((multi_buffer.buffer(buffer.remote_id()).unwrap(), selection)) + let selection = text::ToPoint::to_point(&range.start, &buffer).row + ..text::ToPoint::to_point(&range.end, &buffer).row; + Some(( + multi_buffer.buffer(buffer.remote_id()).unwrap().clone(), + selection, + )) }); let Some((buffer, selection)) = buffer_and_selection else { return Task::ready(Err(anyhow!("failed to determine buffer and selection"))); }; - let Some(project) = self.project() else { + let Some(project) = self.project.as_ref() else { return Task::ready(Err(anyhow!("editor does not have project"))); }; @@ -19084,10 +18946,10 @@ impl Editor { cx: &mut Context, ) { let selection = self.selections.newest::(cx).start.row + 1; - if let Some(file) = self.target_file(cx) - && let Some(path) = file.path().to_str() - { - cx.write_to_clipboard(ClipboardItem::new_string(format!("{path}:{selection}"))); + if let Some(file) = self.target_file(cx) { + if let Some(path) = file.path().to_str() { + cx.write_to_clipboard(ClipboardItem::new_string(format!("{path}:{selection}"))); + } } } @@ -19166,7 +19028,7 @@ impl Editor { (selection.range(), uuid.to_string()) }); this.edit(edits, cx); - this.refresh_edit_prediction(true, false, window, cx); + this.refresh_inline_completion(true, false, window, cx); }); } @@ -19191,7 +19053,7 @@ impl Editor { let locations = self .selections .all_anchors(cx) - .iter() + .into_iter() .map(|selection| Location { buffer: buffer.clone(), range: selection.start.text_anchor..selection.end.text_anchor, @@ -19262,7 +19124,7 @@ impl Editor { row_highlights.insert( ix, RowHighlight { - range, + range: range.clone(), index, color, options, @@ -19638,7 +19500,7 @@ impl Editor { pub fn has_background_highlights(&self) -> bool { self.background_highlights .get(&HighlightKey::Type(TypeId::of::())) - .is_some_and(|(_, highlights)| !highlights.is_empty()) + .map_or(false, |(_, highlights)| !highlights.is_empty()) } pub fn background_highlights_in_range( @@ -19727,10 +19589,10 @@ impl Editor { break; } let end = range.end.to_point(&display_snapshot.buffer_snapshot); - if let Some(current_row) = &end_row - && end.row == current_row.row - { - continue; + if let Some(current_row) = &end_row { + if end.row == current_row.row { + continue; + } } let start = range.start.to_point(&display_snapshot.buffer_snapshot); if start_row.is_none() { @@ -19903,8 +19765,11 @@ impl Editor { event: &SessionEvent, cx: &mut Context, ) { - if let SessionEvent::InvalidateInlineValue = event { - self.refresh_inline_values(cx); + match event { + SessionEvent::InvalidateInlineValue => { + self.refresh_inline_values(cx); + } + _ => {} } } @@ -20016,19 +19881,20 @@ impl Editor { self.refresh_selected_text_highlights(true, window, cx); self.refresh_single_line_folds(window, cx); refresh_matching_bracket_highlights(self, window, cx); - if self.has_active_edit_prediction() { - self.update_visible_edit_prediction(window, cx); + if self.has_active_inline_completion() { + self.update_visible_inline_completion(window, cx); } - if let Some(project) = self.project.as_ref() - && let Some(edited_buffer) = edited_buffer - { - project.update(cx, |project, cx| { - self.registered_buffers - .entry(edited_buffer.read(cx).remote_id()) - .or_insert_with(|| { - project.register_buffer_with_language_servers(edited_buffer, cx) - }); - }); + if let Some(project) = self.project.as_ref() { + if let Some(edited_buffer) = edited_buffer { + project.update(cx, |project, cx| { + self.registered_buffers + .entry(edited_buffer.read(cx).remote_id()) + .or_insert_with(|| { + project + .register_buffer_with_language_servers(&edited_buffer, cx) + }); + }); + } } cx.emit(EditorEvent::BufferEdited); cx.emit(SearchEvent::MatchesInvalidated); @@ -20038,10 +19904,10 @@ impl Editor { } if *singleton_buffer_edited { - if let Some(buffer) = edited_buffer - && buffer.read(cx).file().is_none() - { - cx.emit(EditorEvent::TitleChanged); + if let Some(buffer) = edited_buffer { + if buffer.read(cx).file().is_none() { + cx.emit(EditorEvent::TitleChanged); + } } if let Some(project) = &self.project { #[allow(clippy::mutable_key_type)] @@ -20087,17 +19953,17 @@ impl Editor { } => { self.tasks_update_task = Some(self.refresh_runnables(window, cx)); let buffer_id = buffer.read(cx).remote_id(); - if self.buffer.read(cx).diff_for(buffer_id).is_none() - && let Some(project) = &self.project - { - update_uncommitted_diff_for_buffer( - cx.entity(), - project, - [buffer.clone()], - self.buffer.clone(), - cx, - ) - .detach(); + if self.buffer.read(cx).diff_for(buffer_id).is_none() { + if let Some(project) = &self.project { + update_uncommitted_diff_for_buffer( + cx.entity(), + project, + [buffer.clone()], + self.buffer.clone(), + cx, + ) + .detach(); + } } self.update_lsp_data(false, Some(buffer_id), window, cx); cx.emit(EditorEvent::ExcerptsAdded { @@ -20218,7 +20084,7 @@ impl Editor { } self.tasks_update_task = Some(self.refresh_runnables(window, cx)); self.update_edit_prediction_settings(cx); - self.refresh_edit_prediction(true, false, window, cx); + self.refresh_inline_completion(true, false, window, cx); self.refresh_inline_values(cx); self.refresh_inlay_hints( InlayHintRefreshReason::SettingsChange(inlay_hint_settings( @@ -20230,7 +20096,6 @@ impl Editor { ); let old_cursor_shape = self.cursor_shape; - let old_show_breadcrumbs = self.show_breadcrumbs; { let editor_settings = EditorSettings::get_global(cx); @@ -20244,10 +20109,6 @@ impl Editor { cx.emit(EditorEvent::CursorShapeChanged); } - if old_show_breadcrumbs != self.show_breadcrumbs { - cx.emit(EditorEvent::BreadcrumbsChanged); - } - let project_settings = ProjectSettings::get_global(cx); self.serialize_dirty_buffers = !self.mode.is_minimap() && project_settings.session.restore_unsaved_buffers; @@ -20445,8 +20306,11 @@ impl Editor { .range_to_buffer_ranges_with_deleted_hunks(selection.range()) { if let Some(anchor) = anchor { - let Some(buffer_handle) = multi_buffer.buffer_for_anchor(anchor, cx) - else { + // selection is in a deleted hunk + let Some(buffer_id) = anchor.buffer_id else { + continue; + }; + let Some(buffer_handle) = multi_buffer.buffer(buffer_id) else { continue; }; let offset = text::ToOffset::to_offset( @@ -20554,7 +20418,7 @@ impl Editor { // For now, don't allow opening excerpts in buffers that aren't backed by // regular project files. fn can_open_excerpts_in_file(file: Option<&Arc>) -> bool { - file.is_none_or(|file| project::File::from_dyn(Some(file)).is_some()) + file.map_or(true, |file| project::File::from_dyn(Some(file)).is_some()) } fn marked_text_ranges(&self, cx: &App) -> Option>> { @@ -20597,7 +20461,7 @@ impl Editor { fn report_editor_event( &self, - reported_event: ReportEditorEvent, + event_type: &'static str, file_extension: Option, cx: &App, ) { @@ -20631,30 +20495,15 @@ impl Editor { .show_edit_predictions; let project = project.read(cx); - let event_type = reported_event.event_type(); - - if let ReportEditorEvent::Saved { auto_saved } = reported_event { - telemetry::event!( - event_type, - type = if auto_saved {"autosave"} else {"manual"}, - file_extension, - vim_mode, - copilot_enabled, - copilot_enabled_for_language, - edit_predictions_provider, - is_via_ssh = project.is_via_ssh(), - ); - } else { - telemetry::event!( - event_type, - file_extension, - vim_mode, - copilot_enabled, - copilot_enabled_for_language, - edit_predictions_provider, - is_via_ssh = project.is_via_ssh(), - ); - }; + telemetry::event!( + event_type, + file_extension, + vim_mode, + copilot_enabled, + copilot_enabled_for_language, + edit_predictions_provider, + is_via_ssh = project.is_via_ssh(), + ); } /// Copy the highlighted chunks to the clipboard as JSON. The format is an array of lines, @@ -20698,11 +20547,11 @@ impl Editor { let mut chunk_lines = chunk.text.split('\n').peekable(); while let Some(text) = chunk_lines.next() { let mut merged_with_last_token = false; - if let Some(last_token) = line.back_mut() - && last_token.highlight == highlight - { - last_token.text.push_str(text); - merged_with_last_token = true; + if let Some(last_token) = line.back_mut() { + if last_token.highlight == highlight { + last_token.text.push_str(text); + merged_with_last_token = true; + } } if !merged_with_last_token { @@ -20867,7 +20716,7 @@ impl Editor { { self.hide_context_menu(window, cx); } - self.discard_edit_prediction(false, cx); + self.discard_inline_completion(false, cx); cx.emit(EditorEvent::Blurred); cx.notify(); } @@ -20892,7 +20741,7 @@ impl Editor { let existing_pending = self .text_highlights::(cx) - .map(|(_, ranges)| ranges.to_vec()); + .map(|(_, ranges)| ranges.iter().cloned().collect::>()); if existing_pending.is_none() && pending.is_empty() { return; } @@ -21007,7 +20856,7 @@ impl Editor { cx: &mut Context, ) { let workspace = self.workspace(); - let project = self.project(); + let project = self.project.as_ref(); let save_tasks = self.buffer().update(cx, |multi_buffer, cx| { let mut tasks = Vec::new(); for (buffer_id, changes) in revert_changes { @@ -21045,7 +20894,7 @@ impl Editor { }; if let Some((workspace, path)) = workspace.as_ref().zip(path) { let Some(task) = cx - .update_window_entity(workspace, |workspace, window, cx| { + .update_window_entity(&workspace, |workspace, window, cx| { workspace .open_path_preview(path, None, false, false, false, window, cx) }) @@ -21097,7 +20946,7 @@ impl Editor { pub fn has_visible_completions_menu(&self) -> bool { !self.edit_prediction_preview_is_active() - && self.context_menu.borrow().as_ref().is_some_and(|menu| { + && self.context_menu.borrow().as_ref().map_or(false, |menu| { menu.visible() && matches!(menu, CodeContextMenu::Completions(_)) }) } @@ -21161,37 +21010,39 @@ impl Editor { { let buffer_snapshot = OnceCell::new(); - if let Some(folds) = DB.get_editor_folds(item_id, workspace_id).log_err() - && !folds.is_empty() - { - let snapshot = buffer_snapshot.get_or_init(|| self.buffer.read(cx).snapshot(cx)); - self.fold_ranges( - folds - .into_iter() - .map(|(start, end)| { - snapshot.clip_offset(start, Bias::Left) - ..snapshot.clip_offset(end, Bias::Right) - }) - .collect(), - false, - window, - cx, - ); + if let Some(folds) = DB.get_editor_folds(item_id, workspace_id).log_err() { + if !folds.is_empty() { + let snapshot = + buffer_snapshot.get_or_init(|| self.buffer.read(cx).snapshot(cx)); + self.fold_ranges( + folds + .into_iter() + .map(|(start, end)| { + snapshot.clip_offset(start, Bias::Left) + ..snapshot.clip_offset(end, Bias::Right) + }) + .collect(), + false, + window, + cx, + ); + } } - if let Some(selections) = DB.get_editor_selections(item_id, workspace_id).log_err() - && !selections.is_empty() - { - let snapshot = buffer_snapshot.get_or_init(|| self.buffer.read(cx).snapshot(cx)); - // skip adding the initial selection to selection history - self.selection_history.mode = SelectionHistoryMode::Skipping; - self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_ranges(selections.into_iter().map(|(start, end)| { - snapshot.clip_offset(start, Bias::Left) - ..snapshot.clip_offset(end, Bias::Right) - })); - }); - self.selection_history.mode = SelectionHistoryMode::Normal; + if let Some(selections) = DB.get_editor_selections(item_id, workspace_id).log_err() { + if !selections.is_empty() { + let snapshot = + buffer_snapshot.get_or_init(|| self.buffer.read(cx).snapshot(cx)); + // skip adding the initial selection to selection history + self.selection_history.mode = SelectionHistoryMode::Skipping; + self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges(selections.into_iter().map(|(start, end)| { + snapshot.clip_offset(start, Bias::Left) + ..snapshot.clip_offset(end, Bias::Right) + })); + }); + self.selection_history.mode = SelectionHistoryMode::Normal; + } }; } @@ -21233,15 +21084,17 @@ fn process_completion_for_edit( let mut snippet_source = completion.new_text.clone(); let mut previous_point = text::ToPoint::to_point(cursor_position, buffer); previous_point.column = previous_point.column.saturating_sub(1); - if let Some(scope) = buffer_snapshot.language_scope_at(previous_point) - && scope.prefers_label_for_snippet_in_completion() - && let Some(label) = completion.label() - && matches!( - completion.kind(), - Some(CompletionItemKind::FUNCTION) | Some(CompletionItemKind::METHOD) - ) - { - snippet_source = label; + if let Some(scope) = buffer_snapshot.language_scope_at(previous_point) { + if scope.prefers_label_for_snippet_in_completion() { + if let Some(label) = completion.label() { + if matches!( + completion.kind(), + Some(CompletionItemKind::FUNCTION) | Some(CompletionItemKind::METHOD) + ) { + snippet_source = label; + } + } + } } match Snippet::parse(&snippet_source).log_err() { Some(parsed_snippet) => (Some(parsed_snippet.clone()), parsed_snippet.text), @@ -21265,17 +21118,24 @@ fn process_completion_for_edit( debug_assert!( insert_range .start - .cmp(cursor_position, &buffer_snapshot) + .cmp(&cursor_position, &buffer_snapshot) .is_le(), "insert_range should start before or at cursor position" ); debug_assert!( replace_range .start - .cmp(cursor_position, &buffer_snapshot) + .cmp(&cursor_position, &buffer_snapshot) .is_le(), "replace_range should start before or at cursor position" ); + debug_assert!( + insert_range + .end + .cmp(&cursor_position, &buffer_snapshot) + .is_le(), + "insert_range should end before or at cursor position" + ); let should_replace = match intent { CompletionIntent::CompleteWithInsert => false, @@ -21295,10 +21155,10 @@ fn process_completion_for_edit( ); let mut current_needle = text_to_replace.next(); for haystack_ch in completion.label.text.chars() { - if let Some(needle_ch) = current_needle - && haystack_ch.eq_ignore_ascii_case(&needle_ch) - { - current_needle = text_to_replace.next(); + if let Some(needle_ch) = current_needle { + if haystack_ch.eq_ignore_ascii_case(&needle_ch) { + current_needle = text_to_replace.next(); + } } } current_needle.is_none() @@ -21306,7 +21166,7 @@ fn process_completion_for_edit( LspInsertMode::ReplaceSuffix => { if replace_range .end - .cmp(cursor_position, &buffer_snapshot) + .cmp(&cursor_position, &buffer_snapshot) .is_gt() { let range_after_cursor = *cursor_position..replace_range.end; @@ -21342,7 +21202,7 @@ fn process_completion_for_edit( if range_to_replace .end - .cmp(cursor_position, &buffer_snapshot) + .cmp(&cursor_position, &buffer_snapshot) .is_lt() { range_to_replace.end = *cursor_position; @@ -21350,7 +21210,7 @@ fn process_completion_for_edit( CompletionEdit { new_text, - replace_range: range_to_replace.to_offset(buffer), + replace_range: range_to_replace.to_offset(&buffer), snippet, } } @@ -21520,9 +21380,9 @@ fn is_grapheme_whitespace(text: &str) -> bool { } fn should_stay_with_preceding_ideograph(text: &str) -> bool { - text.chars() - .next() - .is_some_and(|ch| matches!(ch, '。' | '、' | ',' | '?' | '!' | ':' | ';' | '…')) + text.chars().next().map_or(false, |ch| { + matches!(ch, '。' | '、' | ',' | '?' | '!' | ':' | ';' | '…') + }) } #[derive(PartialEq, Eq, Debug, Clone, Copy)] @@ -21552,20 +21412,20 @@ impl<'a> Iterator for WordBreakingTokenizer<'a> { offset += first_grapheme.len(); grapheme_len += 1; if is_grapheme_ideographic(first_grapheme) && !is_whitespace { - if let Some(grapheme) = iter.peek().copied() - && should_stay_with_preceding_ideograph(grapheme) - { - offset += grapheme.len(); - grapheme_len += 1; + if let Some(grapheme) = iter.peek().copied() { + if should_stay_with_preceding_ideograph(grapheme) { + offset += grapheme.len(); + grapheme_len += 1; + } } } else { let mut words = self.input[offset..].split_word_bound_indices().peekable(); let mut next_word_bound = words.peek().copied(); - if next_word_bound.is_some_and(|(i, _)| i == 0) { + if next_word_bound.map_or(false, |(i, _)| i == 0) { next_word_bound = words.next(); } while let Some(grapheme) = iter.peek().copied() { - if next_word_bound.is_some_and(|(i, _)| i == offset) { + if next_word_bound.map_or(false, |(i, _)| i == offset) { break; }; if is_grapheme_whitespace(grapheme) != is_whitespace @@ -21686,7 +21546,7 @@ fn wrap_with_prefix( let subsequent_lines_prefix_len = char_len_with_expanded_tabs(0, &subsequent_lines_prefix, tab_size); let mut wrapped_text = String::new(); - let mut current_line = first_line_prefix; + let mut current_line = first_line_prefix.clone(); let mut is_first_line = true; let tokenizer = WordBreakingTokenizer::new(&unwrapped_text); @@ -21858,7 +21718,7 @@ pub trait SemanticsProvider { buffer: &Entity, position: text::Anchor, cx: &mut App, - ) -> Option>>>; + ) -> Option>>; fn inline_values( &self, @@ -21897,7 +21757,7 @@ pub trait SemanticsProvider { position: text::Anchor, kind: GotoDefinitionKind, cx: &mut App, - ) -> Option>>>>; + ) -> Option>>>; fn range_for_rename( &self, @@ -22010,13 +21870,7 @@ impl CodeActionProvider for Entity { Ok(code_lens_actions .context("code lens fetch")? .into_iter() - .flatten() - .chain( - code_actions - .context("code action fetch")? - .into_iter() - .flatten(), - ) + .chain(code_actions.context("code action fetch")?) .collect()) }) }) @@ -22105,7 +21959,7 @@ fn snippet_completions( snippet .prefix .iter() - .map(move |prefix| StringMatchCandidate::new(ix, prefix)) + .map(move |prefix| StringMatchCandidate::new(ix, &prefix)) }) .collect::>(); @@ -22311,7 +22165,7 @@ impl SemanticsProvider for Entity { buffer: &Entity, position: text::Anchor, cx: &mut App, - ) -> Option>>> { + ) -> Option>> { Some(self.update(cx, |project, cx| project.hover(buffer, position, cx))) } @@ -22332,16 +22186,17 @@ impl SemanticsProvider for Entity { position: text::Anchor, kind: GotoDefinitionKind, cx: &mut App, - ) -> Option>>>> { + ) -> Option>>> { Some(self.update(cx, |project, cx| match kind { - GotoDefinitionKind::Symbol => project.definitions(buffer, position, cx), - GotoDefinitionKind::Declaration => project.declarations(buffer, position, cx), - GotoDefinitionKind::Type => project.type_definitions(buffer, position, cx), - GotoDefinitionKind::Implementation => project.implementations(buffer, position, cx), + GotoDefinitionKind::Symbol => project.definitions(&buffer, position, cx), + GotoDefinitionKind::Declaration => project.declarations(&buffer, position, cx), + GotoDefinitionKind::Type => project.type_definitions(&buffer, position, cx), + GotoDefinitionKind::Implementation => project.implementations(&buffer, position, cx), })) } fn supports_inlay_hints(&self, buffer: &Entity, cx: &mut App) -> bool { + // TODO: make this work for remote projects self.update(cx, |project, cx| { if project .active_debug_session(cx) @@ -22868,7 +22723,6 @@ pub enum EditorEvent { }, Reloaded, CursorShapeChanged, - BreadcrumbsChanged, PushedToNavHistory { anchor: Anchor, is_deactivate: bool, @@ -22888,7 +22742,7 @@ impl Render for Editor { let settings = ThemeSettings::get_global(cx); let mut text_style = match self.mode { - EditorMode::SingleLine | EditorMode::AutoHeight { .. } => TextStyle { + EditorMode::SingleLine { .. } | EditorMode::AutoHeight { .. } => TextStyle { color: cx.theme().colors().editor_foreground, font_family: settings.ui_font.family.clone(), font_features: settings.ui_font.features.clone(), @@ -22914,7 +22768,7 @@ impl Render for Editor { } let background = match self.mode { - EditorMode::SingleLine => cx.theme().system().transparent, + EditorMode::SingleLine { .. } => cx.theme().system().transparent, EditorMode::AutoHeight { .. } => cx.theme().system().transparent, EditorMode::Full { .. } => cx.theme().colors().editor_background, EditorMode::Minimap { .. } => cx.theme().colors().editor_background.opacity(0.7), @@ -22931,7 +22785,7 @@ impl Render for Editor { syntax: cx.theme().syntax().clone(), status: cx.theme().status().clone(), inlay_hints_style: make_inlay_hints_style(cx), - edit_prediction_styles: make_suggestion_styles(cx), + inline_completion_styles: make_suggestion_styles(cx), unnecessary_code_fade: ThemeSettings::get_global(cx).unnecessary_code_fade, show_underlines: self.diagnostics_enabled(), }, @@ -23326,7 +23180,7 @@ impl InvalidationRegion for SnippetState { } } -fn edit_prediction_edit_text( +fn inline_completion_edit_text( current_snapshot: &BufferSnapshot, edits: &[(Range, String)], edit_preview: &EditPreview, @@ -23346,33 +23200,6 @@ fn edit_prediction_edit_text( edit_preview.highlight_edits(current_snapshot, &edits, include_deletions, cx) } -fn edit_prediction_fallback_text(edits: &[(Range, String)], cx: &App) -> HighlightedText { - // Fallback for providers that don't provide edit_preview (like Copilot/Supermaven) - // Just show the raw edit text with basic styling - let mut text = String::new(); - let mut highlights = Vec::new(); - - let insertion_highlight_style = HighlightStyle { - color: Some(cx.theme().colors().text), - ..Default::default() - }; - - for (_, edit_text) in edits { - let start_offset = text.len(); - text.push_str(edit_text); - let end_offset = text.len(); - - if start_offset < end_offset { - highlights.push((start_offset..end_offset, insertion_highlight_style)); - } - } - - HighlightedText { - text: text.into(), - highlights, - } -} - pub fn diagnostic_style(severity: lsp::DiagnosticSeverity, colors: &StatusColors) -> Hsla { match severity { lsp::DiagnosticSeverity::ERROR => colors.error, @@ -23746,7 +23573,7 @@ fn all_edits_insertions_or_deletions( let mut all_deletions = true; for (range, new_text) in edits.iter() { - let range_is_empty = range.to_offset(snapshot).is_empty(); + let range_is_empty = range.to_offset(&snapshot).is_empty(); let text_is_empty = new_text.is_empty(); if range_is_empty != text_is_empty { diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 1d7e04cae0..14f46c0e60 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -20,7 +20,6 @@ pub struct EditorSettings { pub lsp_highlight_debounce: u64, pub hover_popover_enabled: bool, pub hover_popover_delay: u64, - pub status_bar: StatusBar, pub toolbar: Toolbar, pub scrollbar: Scrollbar, pub minimap: Minimap, @@ -126,18 +125,6 @@ pub struct JupyterContent { pub enabled: Option, } -#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -pub struct StatusBar { - /// Whether to display the active language button in the status bar. - /// - /// 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)] pub struct Toolbar { pub breadcrumbs: bool, @@ -453,8 +440,6 @@ pub struct EditorSettingsContent { /// /// Default: 300 pub hover_popover_delay: Option, - /// Status bar related settings - pub status_bar: Option, /// Toolbar related settings pub toolbar: Option, /// Scrollbar related settings @@ -582,19 +567,6 @@ pub struct EditorSettingsContent { pub lsp_document_colors: Option, } -// Status bar related settings -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -pub struct StatusBarContent { - /// Whether to display the active language button in the status bar. - /// - /// Default: true - pub active_language_button: Option, - /// Whether to show the cursor position button in the status bar. - /// - /// Default: true - pub cursor_position_button: Option, -} - // Toolbar related settings #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct ToolbarContent { @@ -810,8 +782,10 @@ 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 { diff --git a/crates/editor/src/editor_settings_controls.rs b/crates/editor/src/editor_settings_controls.rs index 91022d94a8..dc5557b052 100644 --- a/crates/editor/src/editor_settings_controls.rs +++ b/crates/editor/src/editor_settings_controls.rs @@ -88,7 +88,7 @@ impl RenderOnce for BufferFontFamilyControl { .child(Icon::new(IconName::Font)) .child(DropdownMenu::new( "buffer-font-family", - value, + value.clone(), ContextMenu::build(window, cx, |mut menu, _, cx| { let font_family_cache = FontFamilyCache::global(cx); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 2cfdb92593..1a4f444275 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -2,7 +2,7 @@ use super::*; use crate::{ JoinLines, code_context_menus::CodeContextMenu, - edit_prediction_tests::FakeEditPredictionProvider, + inline_completion_tests::FakeInlineCompletionProvider, linked_editing_ranges::LinkedEditingRanges, scroll::scroll_amount::ScrollAmount, test::{ @@ -57,9 +57,7 @@ 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] @@ -76,7 +74,7 @@ fn test_edit_events(cx: &mut TestAppContext) { let editor1 = cx.add_window({ let events = events.clone(); |window, cx| { - let entity = cx.entity(); + let entity = cx.entity().clone(); cx.subscribe_in( &entity, window, @@ -97,7 +95,7 @@ fn test_edit_events(cx: &mut TestAppContext) { let events = events.clone(); |window, cx| { cx.subscribe_in( - &cx.entity(), + &cx.entity().clone(), window, move |_, _, event: &EditorEvent, _, _| match event { EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")), @@ -710,7 +708,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, window, cx); + let mut editor = build_editor(buffer.clone(), window, cx); let handle = cx.entity(); editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle))); @@ -900,7 +898,7 @@ fn test_fold_action(cx: &mut TestAppContext) { .unindent(), cx, ); - build_editor(buffer, window, cx) + build_editor(buffer.clone(), window, cx) }); _ = editor.update(cx, |editor, window, cx| { @@ -991,7 +989,7 @@ fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) { .unindent(), cx, ); - build_editor(buffer, window, cx) + build_editor(buffer.clone(), window, cx) }); _ = editor.update(cx, |editor, window, cx| { @@ -1076,7 +1074,7 @@ fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) { .unindent(), cx, ); - build_editor(buffer, window, cx) + build_editor(buffer.clone(), window, cx) }); _ = editor.update(cx, |editor, window, cx| { @@ -1175,7 +1173,7 @@ fn test_fold_at_level(cx: &mut TestAppContext) { .unindent(), cx, ); - build_editor(buffer, window, cx) + build_editor(buffer.clone(), window, cx) }); _ = editor.update(cx, |editor, window, cx| { @@ -1337,7 +1335,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, window, cx) + build_editor(buffer.clone(), window, cx) }); assert_eq!('🟥'.len_utf8(), 4); @@ -1454,7 +1452,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, window, cx) + build_editor(buffer.clone(), window, cx) }); _ = editor.update(cx, |editor, window, cx| { editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { @@ -1903,51 +1901,6 @@ fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) { }); } -#[gpui::test] -fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let move_to_beg = MoveToBeginningOfLine { - stop_at_soft_wraps: true, - stop_at_indent: true, - }; - - let editor = cx.add_window(|window, cx| { - let buffer = MultiBuffer::build_simple(" hello\nworld", cx); - build_editor(buffer, window, cx) - }); - - _ = editor.update(cx, |editor, window, cx| { - // test cursor between line_start and indent_start - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_display_ranges([ - DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3) - ]); - }); - - // cursor should move to line_start - editor.move_to_beginning_of_line(&move_to_beg, window, cx); - assert_eq!( - editor.selections.display_ranges(cx), - &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)] - ); - - // cursor should move to indent_start - editor.move_to_beginning_of_line(&move_to_beg, window, cx); - assert_eq!( - editor.selections.display_ranges(cx), - &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)] - ); - - // cursor should move to back to line_start - editor.move_to_beginning_of_line(&move_to_beg, window, cx); - assert_eq!( - editor.selections.display_ranges(cx), - &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)] - ); - }); -} - #[gpui::test] fn test_prev_next_word_boundary(cx: &mut TestAppContext) { init_test(cx, |_| {}); @@ -2481,7 +2434,7 @@ fn test_delete_to_word_boundary(cx: &mut TestAppContext) { let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("one two three four", cx); - build_editor(buffer, window, cx) + build_editor(buffer.clone(), window, cx) }); _ = editor.update(cx, |editor, window, cx| { @@ -2529,7 +2482,7 @@ fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) { let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx); - build_editor(buffer, window, cx) + build_editor(buffer.clone(), window, cx) }); let del_to_prev_word_start = DeleteToPreviousWordStart { ignore_newlines: false, @@ -2565,7 +2518,7 @@ fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) { let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx); - build_editor(buffer, window, cx) + build_editor(buffer.clone(), window, cx) }); let del_to_next_word_end = DeleteToNextWordEnd { ignore_newlines: false, @@ -2610,7 +2563,7 @@ fn test_newline(cx: &mut TestAppContext) { let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx); - build_editor(buffer, window, cx) + build_editor(buffer.clone(), window, cx) }); _ = editor.update(cx, |editor, window, cx| { @@ -2646,7 +2599,7 @@ fn test_newline_with_old_selections(cx: &mut TestAppContext) { .as_str(), cx, ); - let mut editor = build_editor(buffer, window, cx); + let mut editor = build_editor(buffer.clone(), window, cx); editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([ Point::new(2, 4)..Point::new(2, 5), @@ -3177,7 +3130,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, window, cx); + let mut editor = build_editor(buffer.clone(), window, cx); editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([3..4, 11..12, 19..20]) }); @@ -5564,7 +5517,7 @@ async fn test_rewrap(cx: &mut TestAppContext) { # ˇThis is a long comment using a pound # sign. "}, - python_language, + python_language.clone(), &mut cx, ); @@ -5671,7 +5624,7 @@ async fn test_rewrap(cx: &mut TestAppContext) { also very long and should not merge with the numbered item.ˇ» "}, - markdown_language, + markdown_language.clone(), &mut cx, ); @@ -5702,7 +5655,7 @@ async fn test_rewrap(cx: &mut TestAppContext) { // This is the second long comment block // to be wrapped.ˇ» "}, - rust_language, + rust_language.clone(), &mut cx, ); @@ -5725,7 +5678,7 @@ async fn test_rewrap(cx: &mut TestAppContext) { «\tThis is a very long indented line \tthat will be wrapped.ˇ» "}, - plaintext_language, + plaintext_language.clone(), &mut cx, ); @@ -6403,7 +6356,7 @@ async fn test_split_selection_into_lines(cx: &mut TestAppContext) { fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) { cx.set_state(initial_state); cx.update_editor(|e, window, cx| { - e.split_selection_into_lines(&Default::default(), window, cx) + e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx) }); cx.assert_editor_state(expected_state); } @@ -6491,7 +6444,7 @@ async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestA DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4), ]) }); - editor.split_selection_into_lines(&Default::default(), window, cx); + editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx); assert_eq!( editor.display_text(cx), "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i" @@ -6507,7 +6460,7 @@ async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestA DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1) ]) }); - editor.split_selection_into_lines(&Default::default(), window, cx); + editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx); assert_eq!( editor.display_text(cx), "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii" @@ -7298,12 +7251,12 @@ async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) { } #[gpui::test] -async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) { +async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; - let provider = cx.new(|_| FakeEditPredictionProvider::default()); + let provider = cx.new(|_| FakeInlineCompletionProvider::default()); cx.update_editor(|editor, window, cx| { editor.set_edit_prediction_provider(Some(provider.clone()), window, cx); }); @@ -7326,7 +7279,7 @@ async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) cx.update(|_, cx| { provider.update(cx, |provider, _| { - provider.set_edit_prediction(Some(edit_prediction::EditPrediction { + provider.set_inline_completion(Some(inline_completion::InlineCompletion { id: None, edits: vec![(edit_position..edit_position, "X".into())], edit_preview: None, @@ -7334,7 +7287,7 @@ async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) }) }); - cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); + cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx)); cx.update_editor(|editor, window, cx| { editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx) }); @@ -8016,29 +7969,6 @@ async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppConte }); } -#[gpui::test] -async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let mut cx = EditorTestContext::new(cx).await; - - let language = Arc::new(Language::new( - LanguageConfig::default(), - Some(tree_sitter_rust::LANGUAGE.into()), - )); - - cx.update_buffer(|buffer, cx| { - buffer.set_language(Some(language), cx); - }); - - cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# }); - cx.update_editor(|editor, window, cx| { - editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx); - }); - - cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# }); -} - #[gpui::test] async fn test_fold_function_bodies(cx: &mut TestAppContext) { init_test(cx, |_| {}); @@ -8207,216 +8137,6 @@ async fn test_autoindent(cx: &mut TestAppContext) { }); } -#[gpui::test] -async fn test_autoindent_disabled(cx: &mut TestAppContext) { - init_test(cx, |settings| settings.defaults.auto_indent = Some(false)); - - let language = Arc::new( - Language::new( - LanguageConfig { - brackets: BracketPairConfig { - pairs: vec![ - BracketPair { - start: "{".to_string(), - end: "}".to_string(), - close: false, - surround: false, - newline: true, - }, - BracketPair { - start: "(".to_string(), - end: ")".to_string(), - close: false, - surround: false, - newline: true, - }, - ], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_rust::LANGUAGE.into()), - ) - .with_indents_query( - r#" - (_ "(" ")" @end) @indent - (_ "{" "}" @end) @indent - "#, - ) - .unwrap(), - ); - - let text = "fn a() {}"; - - let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx)); - let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); - let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx)); - editor - .condition::(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) - .await; - - editor.update_in(cx, |editor, window, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_ranges([5..5, 8..8, 9..9]) - }); - editor.newline(&Newline, window, cx); - assert_eq!( - editor.text(cx), - indoc!( - " - fn a( - - ) { - - } - " - ) - ); - assert_eq!( - editor.selections.ranges(cx), - &[ - Point::new(1, 0)..Point::new(1, 0), - Point::new(3, 0)..Point::new(3, 0), - Point::new(5, 0)..Point::new(5, 0) - ] - ); - }); -} - -#[gpui::test] -async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) { - init_test(cx, |settings| { - settings.defaults.auto_indent = Some(true); - settings.languages.0.insert( - "python".into(), - LanguageSettingsContent { - auto_indent: Some(false), - ..Default::default() - }, - ); - }); - - let mut cx = EditorTestContext::new(cx).await; - - let injected_language = Arc::new( - Language::new( - LanguageConfig { - brackets: BracketPairConfig { - pairs: vec![ - BracketPair { - start: "{".to_string(), - end: "}".to_string(), - close: false, - surround: false, - newline: true, - }, - BracketPair { - start: "(".to_string(), - end: ")".to_string(), - close: true, - surround: false, - newline: true, - }, - ], - ..Default::default() - }, - name: "python".into(), - ..Default::default() - }, - Some(tree_sitter_python::LANGUAGE.into()), - ) - .with_indents_query( - r#" - (_ "(" ")" @end) @indent - (_ "{" "}" @end) @indent - "#, - ) - .unwrap(), - ); - - let language = Arc::new( - Language::new( - LanguageConfig { - brackets: BracketPairConfig { - pairs: vec![ - BracketPair { - start: "{".to_string(), - end: "}".to_string(), - close: false, - surround: false, - newline: true, - }, - BracketPair { - start: "(".to_string(), - end: ")".to_string(), - close: true, - surround: false, - newline: true, - }, - ], - ..Default::default() - }, - name: LanguageName::new("rust"), - ..Default::default() - }, - Some(tree_sitter_rust::LANGUAGE.into()), - ) - .with_indents_query( - r#" - (_ "(" ")" @end) @indent - (_ "{" "}" @end) @indent - "#, - ) - .unwrap() - .with_injection_query( - r#" - (macro_invocation - macro: (identifier) @_macro_name - (token_tree) @injection.content - (#set! injection.language "python")) - "#, - ) - .unwrap(), - ); - - cx.language_registry().add(injected_language); - cx.language_registry().add(language.clone()); - - cx.update_buffer(|buffer, cx| { - buffer.set_language(Some(language), cx); - }); - - cx.set_state(r#"struct A {ˇ}"#); - - cx.update_editor(|editor, window, cx| { - editor.newline(&Default::default(), window, cx); - }); - - cx.assert_editor_state(indoc!( - "struct A { - ˇ - }" - )); - - cx.set_state(r#"select_biased!(ˇ)"#); - - cx.update_editor(|editor, window, cx| { - editor.newline(&Default::default(), window, cx); - editor.handle_input("def ", window, cx); - editor.handle_input("(", window, cx); - editor.newline(&Default::default(), window, cx); - editor.handle_input("a", window, cx); - }); - - cx.assert_editor_state(indoc!( - "select_biased!( - def ( - aˇ - ) - )" - )); -} - #[gpui::test] async fn test_autoindent_selections(cx: &mut TestAppContext) { init_test(cx, |_| {}); @@ -8891,7 +8611,7 @@ async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) { )); cx.language_registry().add(html_language.clone()); - cx.language_registry().add(javascript_language); + cx.language_registry().add(javascript_language.clone()); cx.executor().run_until_parked(); cx.update_buffer(|buffer, cx| { @@ -9635,7 +9355,7 @@ async fn test_snippets(cx: &mut TestAppContext) { .selections .all(cx) .iter() - .map(|s| s.range()) + .map(|s| s.range().clone()) .collect::>(); editor .insert_snippet(&insertion_ranges, snippet, window, cx) @@ -9715,7 +9435,7 @@ async fn test_snippet_indentation(cx: &mut TestAppContext) { .selections .all(cx) .iter() - .map(|s| s.range()) + .map(|s| s.range().clone()) .collect::>(); editor .insert_snippet(&insertion_ranges, snippet, window, cx) @@ -10784,7 +10504,7 @@ async fn test_multiple_formatters(cx: &mut TestAppContext) { kind: Some("code-action-2".into()), edit: Some(lsp::WorkspaceEdit::new( [( - uri, + uri.clone(), vec![lsp::TextEdit::new( lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)), "applied-code-action-2-edit\n".to_string(), @@ -12239,7 +11959,6 @@ async fn test_completion_mode(cx: &mut TestAppContext) { settings.defaults.completions = Some(CompletionSettings { lsp_insert_mode, words: WordsCompletionMode::Disabled, - words_min_length: 0, lsp: true, lsp_fetch_timeout_ms: 0, }); @@ -12298,7 +12017,6 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) update_test_language_settings(&mut cx, |settings| { settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Disabled, - words_min_length: 0, // set the opposite here to ensure that the action is overriding the default behavior lsp_insert_mode: LspInsertMode::Insert, lsp: true, @@ -12314,7 +12032,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) let counter = Arc::new(AtomicUsize::new(0)); handle_completion_request_with_insert_and_replace( &mut cx, - buffer_marked_text, + &buffer_marked_text, vec![(completion_text, completion_text)], counter.clone(), ) @@ -12328,14 +12046,13 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) .confirm_completion_replace(&ConfirmCompletionReplace, window, cx) .unwrap() }); - cx.assert_editor_state(expected_with_replace_mode); + cx.assert_editor_state(&expected_with_replace_mode); handle_resolve_completion_request(&mut cx, None).await; apply_additional_edits.await.unwrap(); update_test_language_settings(&mut cx, |settings| { settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Disabled, - words_min_length: 0, // set the opposite here to ensure that the action is overriding the default behavior lsp_insert_mode: LspInsertMode::Replace, lsp: true, @@ -12349,7 +12066,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) }); handle_completion_request_with_insert_and_replace( &mut cx, - buffer_marked_text, + &buffer_marked_text, vec![(completion_text, completion_text)], counter.clone(), ) @@ -12363,7 +12080,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) .confirm_completion_insert(&ConfirmCompletionInsert, window, cx) .unwrap() }); - cx.assert_editor_state(expected_with_insert_mode); + cx.assert_editor_state(&expected_with_insert_mode); handle_resolve_completion_request(&mut cx, None).await; apply_additional_edits.await.unwrap(); } @@ -13077,7 +12794,6 @@ async fn test_word_completion(cx: &mut TestAppContext) { init_test(cx, |language_settings| { language_settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Fallback, - words_min_length: 0, lsp: true, lsp_fetch_timeout_ms: 10, lsp_insert_mode: LspInsertMode::Insert, @@ -13138,7 +12854,7 @@ async fn test_word_completion(cx: &mut TestAppContext) { if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { assert_eq!( - completion_menu_entries(menu), + completion_menu_entries(&menu), &["first", "last"], "When LSP server is fast to reply, no fallback word completions are used" ); @@ -13161,7 +12877,7 @@ async fn test_word_completion(cx: &mut TestAppContext) { cx.update_editor(|editor, _, _| { if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { - assert_eq!(completion_menu_entries(menu), &["one", "three", "two"], + assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"], "When LSP server is slow, document words can be shown instead, if configured accordingly"); } else { panic!("expected completion menu to be open"); @@ -13174,7 +12890,6 @@ async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext init_test(cx, |language_settings| { language_settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Enabled, - words_min_length: 0, lsp: true, lsp_fetch_timeout_ms: 0, lsp_insert_mode: LspInsertMode::Insert, @@ -13223,7 +12938,7 @@ async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { assert_eq!( - completion_menu_entries(menu), + completion_menu_entries(&menu), &["first", "last", "second"], "Word completions that has the same edit as the any of the LSP ones, should not be proposed" ); @@ -13238,7 +12953,6 @@ async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) { init_test(cx, |language_settings| { language_settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Disabled, - words_min_length: 0, lsp: true, lsp_fetch_timeout_ms: 0, lsp_insert_mode: LspInsertMode::Insert, @@ -13280,7 +12994,7 @@ async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) { if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { assert_eq!( - completion_menu_entries(menu), + completion_menu_entries(&menu), &["first", "last", "second"], "`ShowWordCompletions` action should show word completions" ); @@ -13297,7 +13011,7 @@ async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) { if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { assert_eq!( - completion_menu_entries(menu), + completion_menu_entries(&menu), &["last"], "After showing word completions, further editing should filter them and not query the LSP" ); @@ -13312,7 +13026,6 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) { init_test(cx, |language_settings| { language_settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Fallback, - words_min_length: 0, lsp: false, lsp_fetch_timeout_ms: 0, lsp_insert_mode: LspInsertMode::Insert, @@ -13337,7 +13050,7 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) { if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { assert_eq!( - completion_menu_entries(menu), + completion_menu_entries(&menu), &["let"], "With no digits in the completion query, no digits should be in the word completions" ); @@ -13362,7 +13075,7 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) { cx.update_editor(|editor, _, _| { if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { - assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \ + assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \ return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)"); } else { panic!("expected completion menu to be open"); @@ -13370,56 +13083,6 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) { }); } -#[gpui::test] -async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) { - init_test(cx, |language_settings| { - language_settings.defaults.completions = Some(CompletionSettings { - words: WordsCompletionMode::Enabled, - words_min_length: 3, - lsp: true, - lsp_fetch_timeout_ms: 0, - lsp_insert_mode: LspInsertMode::Insert, - }); - }); - - let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await; - cx.set_state(indoc! {"ˇ - wow - wowen - wowser - "}); - cx.simulate_keystroke("w"); - cx.executor().run_until_parked(); - cx.update_editor(|editor, _, _| { - if editor.context_menu.borrow_mut().is_some() { - panic!( - "expected completion menu to be hidden, as words completion threshold is not met" - ); - } - }); - - cx.simulate_keystroke("o"); - cx.executor().run_until_parked(); - cx.update_editor(|editor, _, _| { - if editor.context_menu.borrow_mut().is_some() { - panic!( - "expected completion menu to be hidden, as words completion threshold is not met still" - ); - } - }); - - cx.simulate_keystroke("w"); - cx.executor().run_until_parked(); - cx.update_editor(|editor, _, _| { - if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() - { - assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word"); - } else { - panic!("expected completion menu to be open after the word completions threshold is met"); - } - }); -} - fn gen_text_edit(params: &CompletionParams, text: &str) -> Option { let position = || lsp::Position { line: params.text_document_position.position.line, @@ -13649,7 +13312,7 @@ async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) { cx.update_editor(|editor, _, _| { if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { - assert_eq!(completion_menu_entries(menu), &["first", "last"]); + assert_eq!(completion_menu_entries(&menu), &["first", "last"]); } else { panic!("expected completion menu to be open"); } @@ -14425,7 +14088,7 @@ async fn test_toggle_block_comment(cx: &mut TestAppContext) { )); cx.language_registry().add(html_language.clone()); - cx.language_registry().add(javascript_language); + cx.language_registry().add(javascript_language.clone()); cx.update_buffer(|buffer, cx| { buffer.set_language(Some(html_language), cx); }); @@ -14602,7 +14265,7 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) { ); let excerpt_ranges = markers.into_iter().map(|marker| { let context = excerpt_ranges.remove(&marker).unwrap()[0].clone(); - ExcerptRange::new(context) + ExcerptRange::new(context.clone()) }); let buffer = cx.new(|cx| Buffer::local(initial_text, cx)); let multibuffer = cx.new(|cx| { @@ -14887,7 +14550,7 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) { let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); - build_editor(buffer, window, cx) + build_editor(buffer.clone(), window, cx) }); _ = editor.update(cx, |editor, window, cx| { @@ -15342,7 +15005,7 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu let mut cx = EditorTestContext::new(cx).await; let lsp_store = - cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store()); + cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store()); cx.set_state(indoc! {" ˇfn func(abc def: i32) -> u32 { @@ -15809,7 +15472,8 @@ async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppCon cx.simulate_keystroke("\n"); cx.run_until_parked(); - let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap()); + let buffer_cloned = + cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap().clone()); let mut request = cx.set_request_handler::(move |_, _, mut cx| { let buffer_cloned = buffer_cloned.clone(); @@ -16751,7 +16415,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestA if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { assert_eq!( - completion_menu_entries(menu), + completion_menu_entries(&menu), &["bg-blue", "bg-red", "bg-yellow"] ); } else { @@ -16764,7 +16428,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestA cx.update_editor(|editor, _, _| { if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { - assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]); + assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]); } else { panic!("expected completion menu to be open"); } @@ -16778,7 +16442,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestA cx.update_editor(|editor, _, _| { if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { - assert_eq!(completion_menu_entries(menu), &["bg-yellow"]); + assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]); } else { panic!("expected completion menu to be open"); } @@ -17347,7 +17011,7 @@ async fn test_multibuffer_reverts(cx: &mut TestAppContext) { (buffer_2.clone(), base_text_2), (buffer_3.clone(), base_text_3), ] { - let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx)); + let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx)); editor .buffer .update(cx, |buffer, cx| buffer.add_diff(diff, cx)); @@ -17968,7 +17632,7 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) { (buffer_2.clone(), file_2_old), (buffer_3.clone(), file_3_old), ] { - let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx)); + let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx)); editor .buffer .update(cx, |buffer, cx| buffer.add_diff(diff, cx)); @@ -19513,7 +19177,7 @@ async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestApp let buffer_id = hunks[0].buffer_id; hunks .into_iter() - .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range)) + .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone())) .collect::>() }); assert_eq!(hunk_ranges.len(), 2); @@ -19604,7 +19268,7 @@ async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestApp let buffer_id = hunks[0].buffer_id; hunks .into_iter() - .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range)) + .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone())) .collect::>() }); assert_eq!(hunk_ranges.len(), 2); @@ -19670,7 +19334,7 @@ async fn test_toggle_deletion_hunk_at_start_of_file( let buffer_id = hunks[0].buffer_id; hunks .into_iter() - .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range)) + .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone())) .collect::>() }); assert_eq!(hunk_ranges.len(), 1); @@ -19693,7 +19357,7 @@ async fn test_toggle_deletion_hunk_at_start_of_file( }); executor.run_until_parked(); - cx.assert_state_with_diff(hunk_expanded); + cx.assert_state_with_diff(hunk_expanded.clone()); } #[gpui::test] @@ -19893,8 +19557,13 @@ fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) { editor.insert_creases(Some(crease), cx); let snapshot = editor.snapshot(window, cx); - let _div = - snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx); + let _div = snapshot.render_crease_toggle( + MultiBufferRow(1), + false, + cx.entity().clone(), + window, + cx, + ); snapshot }) .unwrap(); @@ -20883,7 +20552,7 @@ async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContex } #[gpui::test] -async fn test_edit_prediction_text(cx: &mut TestAppContext) { +async fn test_inline_completion_text(cx: &mut TestAppContext) { init_test(cx, |_| {}); // Simple insertion @@ -20982,7 +20651,7 @@ async fn test_edit_prediction_text(cx: &mut TestAppContext) { } #[gpui::test] -async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) { +async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) { init_test(cx, |_| {}); // Deletion @@ -21072,8 +20741,8 @@ async fn assert_highlighted_edits( .await; cx.update(|_window, cx| { - let highlighted_edits = edit_prediction_edit_text( - snapshot.as_singleton().unwrap().2, + let highlighted_edits = inline_completion_edit_text( + &snapshot.as_singleton().unwrap().2, &edits, &edit_preview, include_deletions, @@ -21089,13 +20758,13 @@ fn assert_breakpoint( path: &Arc, expected: Vec<(u32, Breakpoint)>, ) { - if expected.is_empty() { + if expected.len() == 0usize { assert!(!breakpoints.contains_key(path), "{}", path.display()); } else { let mut breakpoint = breakpoints .get(path) .unwrap() - .iter() + .into_iter() .map(|breakpoint| { ( breakpoint.row, @@ -21124,7 +20793,13 @@ fn add_log_breakpoint_at_cursor( let (anchor, bp) = editor .breakpoints_at_cursors(window, cx) .first() - .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone()))) + .and_then(|(anchor, bp)| { + if let Some(bp) = bp { + Some((*anchor, bp.clone())) + } else { + None + } + }) .unwrap_or_else(|| { let cursor_position: Point = editor.selections.newest(cx).head(); @@ -21134,7 +20809,7 @@ fn add_log_breakpoint_at_cursor( .buffer_snapshot .anchor_before(Point::new(cursor_position.row, 0)); - (breakpoint_position, Breakpoint::new_log(log_message)) + (breakpoint_position, Breakpoint::new_log(&log_message)) }); editor.edit_breakpoint_at_anchor( @@ -21202,7 +20877,7 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) { let abs_path = project.read_with(cx, |project, cx| { project .absolute_path(&project_path, cx) - .map(Arc::from) + .map(|path_buf| Arc::from(path_buf.to_owned())) .unwrap() }); @@ -21220,6 +20895,7 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) + .clone() }); assert_eq!(1, breakpoints.len()); @@ -21244,6 +20920,7 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) + .clone() }); assert_eq!(1, breakpoints.len()); @@ -21265,6 +20942,7 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) + .clone() }); assert_eq!(0, breakpoints.len()); @@ -21316,7 +20994,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { let abs_path = project.read_with(cx, |project, cx| { project .absolute_path(&project_path, cx) - .map(Arc::from) + .map(|path_buf| Arc::from(path_buf.to_owned())) .unwrap() }); @@ -21331,6 +21009,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) + .clone() }); assert_breakpoint( @@ -21351,6 +21030,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) + .clone() }); assert_breakpoint(&breakpoints, &abs_path, vec![]); @@ -21370,6 +21050,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) + .clone() }); assert_breakpoint( @@ -21392,6 +21073,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) + .clone() }); assert_breakpoint( @@ -21414,6 +21096,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) + .clone() }); assert_breakpoint( @@ -21486,7 +21169,7 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) { let abs_path = project.read_with(cx, |project, cx| { project .absolute_path(&project_path, cx) - .map(Arc::from) + .map(|path_buf| Arc::from(path_buf.to_owned())) .unwrap() }); @@ -21506,6 +21189,7 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) + .clone() }); assert_eq!(1, breakpoints.len()); @@ -21537,6 +21221,7 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) + .clone() }); let disable_breakpoint = { @@ -21572,6 +21257,7 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) + .clone() }); assert_eq!(1, breakpoints.len()); @@ -22550,7 +22236,10 @@ async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) { let closing_range = buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8)); let mut linked_ranges = HashMap::default(); - linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]); + linked_ranges.insert( + buffer_id, + vec![(opening_range.clone(), vec![closing_range.clone()])], + ); editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges); }); let mut completion_handle = @@ -22715,7 +22404,7 @@ async fn test_invisible_worktree_servers(cx: &mut TestAppContext) { .await .unwrap(); pane.update_in(cx, |pane, window, cx| { - pane.navigate_backward(&Default::default(), window, cx); + pane.navigate_backward(window, cx); }); cx.run_until_parked(); pane.update(cx, |pane, cx| { @@ -22735,7 +22424,7 @@ async fn test_invisible_worktree_servers(cx: &mut TestAppContext) { ); cx.update(|_, cx| { - workspace::reload(cx); + workspace::reload(&workspace::Reload::default(), cx); }); assert_language_servers_count( 1, @@ -23660,7 +23349,7 @@ pub fn handle_completion_request( complete_from_position ); Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList { - is_incomplete, + is_incomplete: is_incomplete, item_defaults: None, items: completions .iter() @@ -24302,7 +23991,7 @@ async fn test_document_colors(cx: &mut TestAppContext) { workspace .update(cx, |workspace, window, cx| { workspace.active_pane().update(cx, |pane, cx| { - pane.navigate_backward(&Default::default(), window, cx); + pane.navigate_backward(window, cx); }) }) .unwrap(); @@ -24350,41 +24039,6 @@ async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) { }); } -#[gpui::test] -async fn test_non_utf_8_opens(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - cx.update(|cx| { - register_project_item::(cx); - }); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree("/root1", json!({})).await; - fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd]) - .await; - - let project = Project::test(fs, ["/root1".as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - let worktree_id = project.update(cx, |project, cx| { - project.worktrees(cx).next().unwrap().read(cx).id() - }); - - let handle = workspace - .update_in(cx, |workspace, window, cx| { - let project_path = (worktree_id, "one.pdf"); - workspace.open_path(project_path, None, true, window, cx) - }) - .await - .unwrap(); - - assert_eq!( - handle.to_any().entity_type(), - TypeId::of::() - ); -} - #[track_caller] fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec { editor diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 91034829f7..7e77f113ac 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -3,11 +3,11 @@ use crate::{ CodeActionSource, ColumnarMode, ConflictsOurs, ConflictsOursMarker, ConflictsOuter, ConflictsTheirs, ConflictsTheirsMarker, ContextMenuPlacement, CursorShape, CustomBlockId, DisplayDiffHunk, DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, - EditDisplayMode, EditPrediction, Editor, EditorMode, EditorSettings, EditorSnapshot, - EditorStyle, FILE_HEADER_HEIGHT, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, - HandleInput, HoveredCursor, InlayHintRefreshReason, JumpData, LineDown, LineHighlight, LineUp, - MAX_LINE_LEN, MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, PageDown, - PageUp, PhantomBreakpointIndicator, Point, RowExt, RowRangeExt, SelectPhase, + EditDisplayMode, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, + FILE_HEADER_HEIGHT, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, + HoveredCursor, InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight, + LineUp, MAX_LINE_LEN, MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, + PageDown, PageUp, PhantomBreakpointIndicator, Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight, Selection, SelectionDragState, SoftWrap, StickyHeaderExcerpt, ToPoint, ToggleFold, ToggleFoldAll, code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP}, @@ -40,15 +40,14 @@ use git::{ }; use gpui::{ Action, Along, AnyElement, App, AppContext, AvailableSpace, Axis as ScrollbarAxis, BorderStyle, - 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, + 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, 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::{ @@ -61,7 +60,7 @@ use multi_buffer::{ }; use project::{ - Entry, ProjectPath, + ProjectPath, debugger::breakpoint_store::{Breakpoint, BreakpointSessionState}, project_settings::{GitGutterSetting, GitHunkStyleSetting, ProjectSettings}, }; @@ -74,7 +73,6 @@ use std::{ fmt::{self, Write}, iter, mem, ops::{Deref, Range}, - path::{self, Path}, rc::Rc, sync::Arc, time::{Duration, Instant}, @@ -82,17 +80,13 @@ use std::{ use sum_tree::Bias; use text::{BufferId, SelectionGoal}; use theme::{ActiveTheme, Appearance, BufferLineHeight, PlayerColor}; -use ui::{ - ButtonLike, ContextMenu, Indicator, KeyBinding, POPOVER_Y_PADDING, Tooltip, h_flex, prelude::*, - right_click_menu, -}; +use ui::{ButtonLike, KeyBinding, POPOVER_Y_PADDING, Tooltip, h_flex, prelude::*}; use unicode_segmentation::UnicodeSegmentation; use util::post_inc; use util::{RangeExt, ResultExt, debug_panic}; -use workspace::{ - CollaboratorId, ItemSettings, OpenInTerminal, OpenTerminal, RevealInProjectPanel, Workspace, - item::Item, notifications::NotifyTaskExt, -}; +use workspace::{CollaboratorId, Workspace, item::Item, notifications::NotifyTaskExt}; + +const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 7.; /// Determines what kinds of highlights should be applied to a lines background. #[derive(Clone, Copy, Default)] @@ -363,7 +357,6 @@ 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::unwrap_syntax_node); register_action(editor, window, Editor::select_enclosing_symbol); register_action(editor, window, Editor::move_to_enclosing_bracket); register_action(editor, window, Editor::undo_selection); @@ -561,7 +554,7 @@ impl EditorElement { register_action(editor, window, Editor::signature_help_next); register_action(editor, window, Editor::next_edit_prediction); register_action(editor, window, Editor::previous_edit_prediction); - register_action(editor, window, Editor::show_edit_prediction); + register_action(editor, window, Editor::show_inline_completion); register_action(editor, window, Editor::context_menu_first); register_action(editor, window, Editor::context_menu_prev); register_action(editor, window, Editor::context_menu_next); @@ -569,7 +562,7 @@ impl EditorElement { register_action(editor, window, Editor::display_cursor_names); register_action(editor, window, Editor::unique_lines_case_insensitive); register_action(editor, window, Editor::unique_lines_case_sensitive); - register_action(editor, window, Editor::accept_partial_edit_prediction); + register_action(editor, window, Editor::accept_partial_inline_completion); register_action(editor, window, Editor::accept_edit_prediction); register_action(editor, window, Editor::restore_file); register_action(editor, window, Editor::git_restore); @@ -725,7 +718,7 @@ impl EditorElement { ColumnarMode::FromMouse => true, ColumnarMode::FromSelection => false, }, - mode, + mode: mode, goal_column: point_for_position.exact_unclipped.column(), }, window, @@ -918,11 +911,6 @@ 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; } @@ -961,12 +949,8 @@ impl EditorElement { let hovered_link_modifier = Editor::multi_cursor_modifier(false, &event.modifiers(), cx); - if let Some(mouse_position) = event.mouse_position() - && !pending_nonempty_selections - && hovered_link_modifier - && text_hitbox.is_hovered(window) - { - let point = position_map.point_for_position(mouse_position); + if !pending_nonempty_selections && hovered_link_modifier && text_hitbox.is_hovered(window) { + let point = position_map.point_for_position(event.up.position); editor.handle_click_hovered_link(point, event.modifiers(), window, cx); editor.selection_drag_state = SelectionDragState::None; @@ -1128,24 +1112,26 @@ 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 { + 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 + }) + } else { + None + } }; if hovered_diff_hunk_row != editor.hovered_diff_hunk_row { @@ -1159,14 +1145,14 @@ impl EditorElement { .inline_blame_popover .as_ref() .and_then(|state| state.popover_bounds) - .is_some_and(|bounds| bounds.contains(&event.position)); + .map_or(false, |bounds| bounds.contains(&event.position)); let keyboard_grace = editor .inline_blame_popover .as_ref() - .is_some_and(|state| state.keyboard_grace); + .map_or(false, |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(&blame_entry, event.position, false, cx); } else if !keyboard_grace { editor.hide_blame_popover(cx); } @@ -1190,10 +1176,10 @@ impl EditorElement { let is_visible = editor .gutter_breakpoint_indicator .0 - .is_some_and(|indicator| indicator.is_active); + .map_or(false, |indicator| indicator.is_active); let has_existing_breakpoint = - editor.breakpoint_store.as_ref().is_some_and(|store| { + editor.breakpoint_store.as_ref().map_or(false, |store| { let Some(project) = &editor.project else { return false; }; @@ -1391,27 +1377,29 @@ 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)) { - 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])); + 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])); + } } } @@ -1422,15 +1410,19 @@ impl EditorElement { CollaboratorId::PeerId(peer_id) => { if let Some(collaborator) = collaboration_hub.collaborators(cx).get(&peer_id) - && let Some(participant_index) = collaboration_hub + { + if let Some(participant_index) = collaboration_hub .user_participant_indices(cx) .get(&collaborator.user_id) - && let Some((local_selection_style, _)) = selections.first_mut() - { - *local_selection_style = cx - .theme() - .players() - .color_for_participant(participant_index.0); + { + if let Some((local_selection_style, _)) = selections.first_mut() + { + *local_selection_style = cx + .theme() + .players() + .color_for_participant(participant_index.0); + } + } } } CollaboratorId::Agent => { @@ -2101,7 +2093,7 @@ impl EditorElement { row_block_types: &HashMap, content_origin: gpui::Point, scroll_pixel_position: gpui::Point, - edit_prediction_popover_origin: Option>, + inline_completion_popover_origin: Option>, start_row: DisplayRow, end_row: DisplayRow, line_height: Pixels, @@ -2173,13 +2165,11 @@ impl EditorElement { }; let padding = ProjectSettings::get_global(cx).diagnostics.inline.padding as f32 * em_width; - let min_x = self.column_pixels( - ProjectSettings::get_global(cx) - .diagnostics - .inline - .min_column as usize, - window, - ); + let min_x = ProjectSettings::get_global(cx) + .diagnostics + .inline + .min_column as f32 + * em_width; let mut elements = HashMap::default(); for (row, mut diagnostics) in diagnostics_by_rows { @@ -2220,12 +2210,12 @@ impl EditorElement { cmp::max(padded_line, min_start) }; - let behind_edit_prediction_popover = edit_prediction_popover_origin + let behind_inline_completion_popover = inline_completion_popover_origin .as_ref() - .is_some_and(|edit_prediction_popover_origin| { - (pos_y..pos_y + line_height).contains(&edit_prediction_popover_origin.y) + .map_or(false, |inline_completion_popover_origin| { + (pos_y..pos_y + line_height).contains(&inline_completion_popover_origin.y) }); - let opacity = if behind_edit_prediction_popover { + let opacity = if behind_inline_completion_popover { 0.5 } else { 1.0 @@ -2290,7 +2280,9 @@ impl EditorElement { None } }) - .is_some_and(|source| matches!(source, CodeActionSource::Indicator(..))); + .map_or(false, |source| { + matches!(source, CodeActionSource::Indicator(..)) + }); Some(editor.render_inline_code_actions(icon_size, display_point.row(), active, cx)) })?; @@ -2430,21 +2422,19 @@ impl EditorElement { let editor = self.editor.read(cx); let blame = editor.blame.clone()?; let padding = { + const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 6.; const INLINE_ACCEPT_SUGGESTION_EM_WIDTHS: f32 = 14.; - let mut padding = ProjectSettings::get_global(cx) - .git - .inline_blame - .unwrap_or_default() - .padding as f32; + let mut padding = INLINE_BLAME_PADDING_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 + if let Some(inline_completion) = editor.active_inline_completion.as_ref() { + match &inline_completion.completion { + InlineCompletion::Edit { + display_mode: EditDisplayMode::TabAccept, + .. + } => padding += INLINE_ACCEPT_SUGGESTION_EM_WIDTHS, + _ => {} + } } padding * em_width @@ -2473,7 +2463,7 @@ impl EditorElement { let min_column_in_pixels = ProjectSettings::get_global(cx) .git .inline_blame - .map(|settings| settings.min_column) + .and_then(|settings| settings.min_column) .map(|col| self.column_pixels(col as usize, window)) .unwrap_or(px(0.)); let min_start = content_origin.x - scroll_pixel_position.x + min_column_in_pixels; @@ -2750,10 +2740,7 @@ 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 { .. } | Block::BufferHeader { .. } - ) { + if matches!(block, Block::ExcerptBoundary { .. }) { found_excerpt_header = true; break; } @@ -2770,10 +2757,7 @@ 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 { .. } | Block::BufferHeader { .. } - ) { + if matches!(block, Block::ExcerptBoundary { .. }) { found_excerpt_header = true; } block_height += block.height(); @@ -2820,7 +2804,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; } @@ -2911,7 +2895,7 @@ impl EditorElement { if multibuffer_row .0 .checked_sub(1) - .is_some_and(|previous_row| { + .map_or(false, |previous_row| { snapshot.is_line_folded(MultiBufferRow(previous_row)) }) { @@ -2984,8 +2968,8 @@ impl EditorElement { .ilog10() + 1; - buffer_rows - .iter() + let elements = buffer_rows + .into_iter() .enumerate() .map(|(ix, row_info)| { let ExpandInfo { @@ -3020,7 +3004,7 @@ impl EditorElement { .icon_color(Color::Custom(cx.theme().colors().editor_line_number)) .selected_icon_color(Color::Custom(cx.theme().colors().editor_foreground)) .icon_size(IconSize::Custom(rems(editor_font_size / window.rem_size()))) - .width(width) + .width(width.into()) .on_click(move |_, window, cx| { editor.update(cx, |editor, cx| { editor.expand_excerpt(excerpt_id, direction, window, cx); @@ -3040,7 +3024,9 @@ impl EditorElement { Some((toggle, origin)) }) - .collect() + .collect(); + + elements } fn calculate_relative_line_numbers( @@ -3140,7 +3126,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 - .iter() + .into_iter() .enumerate() .flat_map(|(ix, row_info)| { let display_row = DisplayRow(rows.start.0 + ix as u32); @@ -3217,7 +3203,7 @@ impl EditorElement { && self.editor.read(cx).is_singleton(cx); if include_fold_statuses { row_infos - .iter() + .into_iter() .enumerate() .map(|(ix, info)| { if info.expand_info.is_some() { @@ -3312,7 +3298,7 @@ impl EditorElement { let chunks = snapshot.highlighted_chunks(rows.clone(), true, style); LineWithInvisibles::from_chunks( chunks, - style, + &style, MAX_LINE_LEN, rows.len(), &snapshot.mode, @@ -3393,7 +3379,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(), @@ -3459,41 +3445,42 @@ impl EditorElement { .into_any_element() } - Block::ExcerptBoundary { .. } => { + Block::ExcerptBoundary { + excerpt, + height, + starts_new_buffer, + .. + } => { 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 sticky_header_excerpt_id != Some(excerpt.id) { - let selected = selected_buffer_ids.contains(&excerpt.buffer_id); + if *starts_new_buffer { + 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, - ), - )); + 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(div().h(FILE_HEADER_HEIGHT as f32 * window.line_height())); - } + result = result.child( + h_flex().relative().child( + div() + .top(line_height / 2.) + .absolute() + .w_full() + .h_px() + .bg(color.border_variant), + ), + ); + }; result.into_any() } @@ -3517,33 +3504,33 @@ impl EditorElement { let mut x_offset = px(0.); let mut is_block = true; - 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 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 element_height_in_lines != block.height() { - resized_blocks.insert(custom_block_id, element_height_in_lines); } } for i in 0..element_height_in_lines { @@ -3562,10 +3549,11 @@ impl EditorElement { jump_data: JumpData, window: &mut Window, cx: &mut App, - ) -> impl IntoElement { + ) -> Div { let editor = self.editor.read(cx); - let multi_buffer = editor.buffer.read(cx); - let file_status = multi_buffer + let file_status = editor + .buffer + .read(cx) .all_diff_hunks_expanded() .then(|| { editor @@ -3575,17 +3563,6 @@ 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 @@ -3593,17 +3570,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 relative_path = for_excerpt.buffer.resolve_file_path(cx, include_root); - let filename = relative_path + let path = for_excerpt.buffer.resolve_file_path(cx, include_root); + let filename = path .as_ref() .and_then(|path| Some(path.file_name()?.to_string_lossy().to_string())); - let parent_path = relative_path.as_ref().and_then(|path| { + let parent_path = 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(); - let header = div() + div() .p_1() .w_full() .h(FILE_HEADER_HEIGHT as f32 * window.line_height()) @@ -3643,7 +3620,7 @@ impl EditorElement { ButtonLike::new("toggle-buffer-fold") .style(ui::ButtonStyle::Transparent) .height(px(28.).into()) - .width(px(28.)) + .width(px(28.).into()) .children(toggle_chevron_icon) .tooltip({ let focus_handle = focus_handle.clone(); @@ -3693,54 +3670,38 @@ impl EditorElement { }) .take(1), ) - .child( - h_flex() - .size(Pixels(12.0)) - .justify_center() - .children(indicator), - ) .child( h_flex() .cursor_pointer() .id("path header block") .size_full() .justify_between() - .overflow_hidden() .child( h_flex() .gap_2() - .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() - }) - }, - )) - }) + .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()) + }, + ), + ) .when_some(parent_path, |then, path| { then.child(div().child(path).text_color( if file_status.is_some_and(FileStatus::is_deleted) { @@ -3751,139 +3712,36 @@ impl EditorElement { )) }), ) - .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()), - ), - ) - }, - ) + .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()), + ), + ) + }) .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) .on_click(window.listener_for(&self.editor, { move |editor, e: &ClickEvent, window, cx| { editor.open_excerpts_common( Some(jump_data.clone()), - e.modifiers().secondary(), + e.down.modifiers.secondary(), window, cx, ); } })), ), - ); - - let file = for_excerpt.buffer.file().cloned(); - let editor = self.editor.clone(); - right_click_menu("buffer-header-context-menu") - .trigger(move |_, _, _| header) - .menu(move |window, cx| { - let menu_context = focus_handle.clone(); - let editor = editor.clone(); - let file = file.clone(); - ContextMenu::build(window, cx, move |mut menu, window, cx| { - if let Some(file) = file - && let Some(project) = editor.read(cx).project() - && let Some(worktree) = - project.read(cx).worktree_for_id(file.worktree_id(cx), cx) - { - let worktree = worktree.read(cx); - let relative_path = file.path(); - let entry_for_path = worktree.entry_for_path(relative_path); - let abs_path = entry_for_path.map(|e| { - e.canonical_path.as_deref().map_or_else( - || worktree.abs_path().join(relative_path), - Path::to_path_buf, - ) - }); - let has_relative_path = worktree.root_entry().is_some_and(Entry::is_dir); - - let parent_abs_path = abs_path - .as_ref() - .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf())); - let relative_path = has_relative_path - .then_some(relative_path) - .map(ToOwned::to_owned); - - let visible_in_project_panel = - relative_path.is_some() && worktree.is_visible(); - let reveal_in_project_panel = entry_for_path - .filter(|_| visible_in_project_panel) - .map(|entry| entry.id); - menu = menu - .when_some(abs_path, |menu, abs_path| { - menu.entry( - "Copy Path", - Some(Box::new(zed_actions::workspace::CopyPath)), - window.handler_for(&editor, move |_, _, cx| { - cx.write_to_clipboard(ClipboardItem::new_string( - abs_path.to_string_lossy().to_string(), - )); - }), - ) - }) - .when_some(relative_path, |menu, relative_path| { - menu.entry( - "Copy Relative Path", - Some(Box::new(zed_actions::workspace::CopyRelativePath)), - window.handler_for(&editor, move |_, _, cx| { - cx.write_to_clipboard(ClipboardItem::new_string( - relative_path.to_string_lossy().to_string(), - )); - }), - ) - }) - .when( - reveal_in_project_panel.is_some() || parent_abs_path.is_some(), - |menu| menu.separator(), - ) - .when_some(reveal_in_project_panel, |menu, entry_id| { - menu.entry( - "Reveal In Project Panel", - Some(Box::new(RevealInProjectPanel::default())), - window.handler_for(&editor, move |editor, _, cx| { - if let Some(project) = &mut editor.project { - project.update(cx, |_, cx| { - cx.emit(project::Event::RevealInProjectPanel( - entry_id, - )) - }); - } - }), - ) - }) - .when_some(parent_abs_path, |menu, parent_abs_path| { - menu.entry( - "Open in Terminal", - Some(Box::new(OpenInTerminal)), - window.handler_for(&editor, move |_, window, cx| { - window.dispatch_action( - OpenTerminal { - working_directory: parent_abs_path.clone(), - } - .boxed_clone(), - cx, - ); - }), - ) - }); - } - - menu.context(menu_context) - }) - }) + ) } fn render_blocks( @@ -3921,7 +3779,7 @@ impl EditorElement { for (row, block) in fixed_blocks { let block_id = block.id(); - if focused_block.as_ref().is_some_and(|b| b.id == block_id) { + if focused_block.as_ref().map_or(false, |b| b.id == block_id) { focused_block = None; } @@ -3978,7 +3836,7 @@ impl EditorElement { }; let block_id = block.id(); - if focused_block.as_ref().is_some_and(|b| b.id == block_id) { + if focused_block.as_ref().map_or(false, |b| b.id == block_id) { focused_block = None; } @@ -4019,58 +3877,60 @@ impl EditorElement { } } - if let Some(focused_block) = focused_block - && let Some(focus_handle) = focused_block.focus_handle.upgrade() - && focus_handle.is_focused(window) - && let Some(block) = snapshot.block_for_id(focused_block.id) - { - let style = block.style(); - let width = match style { - BlockStyle::Fixed => AvailableSpace::MinContent, - BlockStyle::Flex => AvailableSpace::Definite( - hitbox - .size - .width - .max(fixed_block_max_width) - .max(editor_margins.gutter.width + *scroll_width), - ), - BlockStyle::Sticky => AvailableSpace::Definite(hitbox.size.width), - }; + if let Some(focused_block) = focused_block { + if let Some(focus_handle) = focused_block.focus_handle.upgrade() { + if focus_handle.is_focused(window) { + if let Some(block) = snapshot.block_for_id(focused_block.id) { + let style = block.style(); + let width = match style { + BlockStyle::Fixed => AvailableSpace::MinContent, + BlockStyle::Flex => AvailableSpace::Definite( + hitbox + .size + .width + .max(fixed_block_max_width) + .max(editor_margins.gutter.width + *scroll_width), + ), + BlockStyle::Sticky => AvailableSpace::Definite(hitbox.size.width), + }; - if let Some((element, element_size, _, x_offset)) = self.render_block( - &block, - width, - focused_block.id, - rows.end, - snapshot, - text_x, - &rows, - line_layouts, - editor_margins, - line_height, - em_width, - text_hitbox, - editor_width, - scroll_width, - &mut resized_blocks, - &mut row_block_types, - selections, - selected_buffer_ids, - is_row_soft_wrapped, - sticky_header_excerpt_id, - window, - cx, - ) { - blocks.push(BlockLayout { - id: block.id(), - x_offset, - row: None, - element, - available_space: size(width, element_size.height.into()), - style, - overlaps_gutter: true, - is_buffer_header: block.is_buffer_header(), - }); + if let Some((element, element_size, _, x_offset)) = self.render_block( + &block, + width, + focused_block.id, + rows.end, + snapshot, + text_x, + &rows, + line_layouts, + editor_margins, + line_height, + em_width, + text_hitbox, + editor_width, + scroll_width, + &mut resized_blocks, + &mut row_block_types, + selections, + selected_buffer_ids, + is_row_soft_wrapped, + sticky_header_excerpt_id, + window, + cx, + ) { + blocks.push(BlockLayout { + id: block.id(), + x_offset, + row: None, + element, + available_space: size(width, element_size.height.into()), + style, + overlaps_gutter: true, + is_buffer_header: block.is_buffer_header(), + }); + } + } + } } } @@ -4226,26 +4086,27 @@ impl EditorElement { { let editor = self.editor.read(cx); - if editor.edit_prediction_visible_in_cursor_popover(editor.has_active_edit_prediction()) + if editor + .edit_prediction_visible_in_cursor_popover(editor.has_active_inline_completion()) { height_above_menu += editor.edit_prediction_cursor_popover_height() + POPOVER_Y_PADDING; edit_prediction_popover_visible = true; } - if editor.context_menu_visible() - && let Some(crate::ContextMenuOrigin::Cursor) = editor.context_menu_origin() - { - let (min_height_in_lines, max_height_in_lines) = editor - .context_menu_options - .as_ref() - .map_or((3, 12), |options| { - (options.min_entries_visible, options.max_entries_visible) - }); + if editor.context_menu_visible() { + if let Some(crate::ContextMenuOrigin::Cursor) = editor.context_menu_origin() { + let (min_height_in_lines, max_height_in_lines) = editor + .context_menu_options + .as_ref() + .map_or((3, 12), |options| { + (options.min_entries_visible, options.max_entries_visible) + }); - min_menu_height += line_height * min_height_in_lines as f32 + POPOVER_Y_PADDING; - max_menu_height += line_height * max_height_in_lines as f32 + POPOVER_Y_PADDING; - context_menu_visible = true; + min_menu_height += line_height * min_height_in_lines as f32 + POPOVER_Y_PADDING; + max_menu_height += line_height * max_height_in_lines as f32 + POPOVER_Y_PADDING; + context_menu_visible = true; + } } context_menu_placement = editor .context_menu_options @@ -4757,7 +4618,7 @@ impl EditorElement { } }; - let source_included = source_display_point.is_none_or(|source_display_point| { + let source_included = source_display_point.map_or(true, |source_display_point| { visible_range .to_inclusive() .contains(&source_display_point.row()) @@ -4937,7 +4798,7 @@ impl EditorElement { let intersects_menu = |bounds: Bounds| -> bool { context_menu_layout .as_ref() - .is_some_and(|menu| bounds.intersects(&menu.bounds)) + .map_or(false, |menu| bounds.intersects(&menu.bounds)) }; let can_place_above = { @@ -5122,7 +4983,7 @@ impl EditorElement { if active_positions .iter() - .any(|p| p.is_some_and(|p| display_row_range.contains(&p.row()))) + .any(|p| p.map_or(false, |p| display_row_range.contains(&p.row()))) { let y = display_row_range.start.as_f32() * line_height + text_hitbox.bounds.top() @@ -5235,7 +5096,7 @@ impl EditorElement { let intersects_menu = |bounds: Bounds| -> bool { context_menu_layout .as_ref() - .is_some_and(|menu| bounds.intersects(&menu.bounds)) + .map_or(false, |menu| bounds.intersects(&menu.bounds)) }; let final_origin = if popover_bounds_above.is_contained_within(hitbox) @@ -5320,7 +5181,7 @@ impl EditorElement { let mut end_row = start_row.0; while active_rows .peek() - .is_some_and(|(active_row, has_selection)| { + .map_or(false, |(active_row, has_selection)| { active_row.0 == end_row + 1 && has_selection.selection == contains_non_empty_selection.selection }) @@ -5579,9 +5440,9 @@ impl EditorElement { // In singleton buffers, we select corresponding lines on the line number click, so use | -like cursor. // In multi buffers, we open file at the line number clicked, so use a pointing hand cursor. if is_singleton { - window.set_cursor_style(CursorStyle::IBeam, hitbox); + window.set_cursor_style(CursorStyle::IBeam, &hitbox); } else { - window.set_cursor_style(CursorStyle::PointingHand, hitbox); + window.set_cursor_style(CursorStyle::PointingHand, &hitbox); } } } @@ -5600,7 +5461,7 @@ impl EditorElement { &layout.position_map.snapshot, line_height, layout.gutter_hitbox.bounds, - hunk, + &hunk, ); Some(( hunk_bounds, @@ -5736,10 +5597,7 @@ impl EditorElement { let end_row_in_current_excerpt = snapshot .blocks_in_range(start_row..end_row) .find_map(|(start_row, block)| { - if matches!( - block, - Block::ExcerptBoundary { .. } | Block::BufferHeader { .. } - ) { + if matches!(block, Block::ExcerptBoundary { .. }) { Some(start_row) } else { None @@ -5794,15 +5652,16 @@ impl EditorElement { cx: &mut App, ) { for (_, hunk_hitbox) in &layout.display_hunks { - if let Some(hunk_hitbox) = hunk_hitbox - && !self + if let Some(hunk_hitbox) = hunk_hitbox { + if !self .editor .read(cx) .buffer() .read(cx) .all_diff_hunks_expanded() - { - window.set_cursor_style(CursorStyle::PointingHand, hunk_hitbox); + { + window.set_cursor_style(CursorStyle::PointingHand, hunk_hitbox); + } } } @@ -6124,10 +5983,10 @@ impl EditorElement { if axis == ScrollbarAxis::Vertical { let fast_markers = - self.collect_fast_scrollbar_markers(layout, scrollbar_layout, cx); + self.collect_fast_scrollbar_markers(layout, &scrollbar_layout, cx); // Refresh slow scrollbar markers in the background. Below, we // paint whatever markers have already been computed. - self.refresh_slow_scrollbar_markers(layout, scrollbar_layout, window, cx); + self.refresh_slow_scrollbar_markers(layout, &scrollbar_layout, window, cx); let markers = self.editor.read(cx).scrollbar_marker_state.markers.clone(); for marker in markers.iter().chain(&fast_markers) { @@ -6161,7 +6020,7 @@ impl EditorElement { if any_scrollbar_dragged { window.set_window_cursor_style(CursorStyle::Arrow); } else { - window.set_cursor_style(CursorStyle::Arrow, hitbox); + window.set_cursor_style(CursorStyle::Arrow, &hitbox); } } }) @@ -6711,23 +6570,25 @@ impl EditorElement { editor.set_scroll_position(position, window, cx); } cx.stop_propagation(); - } else if minimap_hitbox.is_hovered(window) { - editor.scroll_manager.set_is_hovering_minimap_thumb( - !event.dragging() - && layout - .thumb_layout - .thumb_bounds - .is_some_and(|bounds| bounds.contains(&event.position)), - cx, - ); - - // Stop hover events from propagating to the - // underlying editor if the minimap hitbox is hovered - if !event.dragging() { - cx.stop_propagation(); - } } else { - editor.scroll_manager.hide_minimap_thumb(cx); + if minimap_hitbox.is_hovered(window) { + editor.scroll_manager.set_is_hovering_minimap_thumb( + !event.dragging() + && layout + .thumb_layout + .thumb_bounds + .is_some_and(|bounds| bounds.contains(&event.position)), + cx, + ); + + // Stop hover events from propagating to the + // underlying editor if the minimap hitbox is hovered + if !event.dragging() { + cx.stop_propagation(); + } + } else { + editor.scroll_manager.hide_minimap_thumb(cx); + } } mouse_position = event.position; }); @@ -6815,14 +6676,14 @@ impl EditorElement { } } - fn paint_edit_prediction_popover( + fn paint_inline_completion_popover( &mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App, ) { - if let Some(edit_prediction_popover) = layout.edit_prediction_popover.as_mut() { - edit_prediction_popover.paint(window, cx); + if let Some(inline_completion_popover) = layout.inline_completion_popover.as_mut() { + inline_completion_popover.paint(window, cx); } } @@ -7021,10 +6882,10 @@ impl EditorElement { // Fire click handlers during the bubble phase. DispatchPhase::Bubble => editor.update(cx, |editor, cx| { if let Some(mouse_down) = captured_mouse_down.take() { - let event = ClickEvent::Mouse(MouseClickEvent { + let event = ClickEvent { down: mouse_down, up: event.clone(), - }); + }; Self::click(editor, &event, &position_map, window, cx); } }), @@ -7106,7 +6967,9 @@ impl EditorElement { let unstaged_hollow = ProjectSettings::get_global(cx) .git .hunk_style - .is_some_and(|style| matches!(style, GitHunkStyleSetting::UnstagedHollow)); + .map_or(false, |style| { + matches!(style, GitHunkStyleSetting::UnstagedHollow) + }); unstaged == unstaged_hollow } @@ -7150,7 +7013,7 @@ fn header_jump_data( pub struct AcceptEditPredictionBinding(pub(crate) Option); impl AcceptEditPredictionBinding { - pub fn keystroke(&self) -> Option<&KeybindingKeystroke> { + pub fn keystroke(&self) -> Option<&Keystroke> { if let Some(binding) = self.0.as_ref() { match &binding.keystrokes() { [keystroke, ..] => Some(keystroke), @@ -7240,7 +7103,7 @@ fn render_blame_entry_popover( ) -> Option { let renderer = cx.global::().0.clone(); let blame = blame.read(cx); - let repository = blame.repository(cx)?; + let repository = blame.repository(cx)?.clone(); renderer.render_blame_entry_popover( blame_entry, scroll_handle, @@ -7945,7 +7808,7 @@ impl Element for EditorElement { min_lines, max_lines, } => { - let editor_handle = cx.entity(); + let editor_handle = cx.entity().clone(); let max_line_number_width = self.max_line_number_width(&editor.snapshot(window, cx), window); window.request_measured_layout( @@ -8136,7 +7999,7 @@ impl Element for EditorElement { // The max scroll position for the top of the window let max_scroll_top = if matches!( snapshot.mode, - EditorMode::SingleLine + EditorMode::SingleLine { .. } | EditorMode::AutoHeight { .. } | EditorMode::Full { sized_by_content: true, @@ -8161,20 +8024,12 @@ impl Element for EditorElement { autoscroll_containing_element, needs_horizontal_autoscroll, ) = self.editor.update(cx, |editor, cx| { - let autoscroll_request = editor.scroll_manager.take_autoscroll_request(); - + let autoscroll_request = editor.autoscroll_request(); let autoscroll_containing_element = autoscroll_request.is_some() || editor.has_pending_selection(); let (needs_horizontal_autoscroll, was_scrolled) = editor - .autoscroll_vertically( - bounds, - line_height, - max_scroll_top, - autoscroll_request, - window, - cx, - ); + .autoscroll_vertically(bounds, line_height, max_scroll_top, window, cx); if was_scrolled.0 { snapshot = editor.snapshot(window, cx); } @@ -8203,7 +8058,7 @@ impl Element for EditorElement { let is_row_soft_wrapped = |row: usize| { row_infos .get(row) - .is_none_or(|info| info.buffer_row.is_none()) + .map_or(true, |info| info.buffer_row.is_none()) }; let start_anchor = if start_row == Default::default() { @@ -8496,13 +8351,7 @@ impl Element for EditorElement { }) .flatten()?; let mut element = render_inline_blame_entry(blame_entry, &style, cx)?; - let inline_blame_padding = ProjectSettings::get_global(cx) - .git - .inline_blame - .unwrap_or_default() - .padding - as f32 - * em_advance; + let inline_blame_padding = INLINE_BLAME_PADDING_EM_WIDTHS * em_advance; Some( element .layout_as_root(AvailableSpace::min_size(), window, cx) @@ -8570,11 +8419,7 @@ impl Element for EditorElement { Ok(blocks) => blocks, Err(resized_blocks) => { self.editor.update(cx, |editor, cx| { - editor.resize_blocks( - resized_blocks, - autoscroll_request.map(|(autoscroll, _)| autoscroll), - cx, - ) + editor.resize_blocks(resized_blocks, autoscroll_request, cx) }); return self.prepaint(None, _inspector_id, bounds, &mut (), window, cx); } @@ -8619,7 +8464,6 @@ impl Element for EditorElement { scroll_width, em_advance, &line_layouts, - autoscroll_request, window, cx, ) @@ -8657,7 +8501,7 @@ impl Element for EditorElement { ) }); - let (edit_prediction_popover, edit_prediction_popover_origin) = self + let (inline_completion_popover, inline_completion_popover_origin) = self .editor .update(cx, |editor, cx| { editor.render_edit_prediction_popover( @@ -8686,7 +8530,7 @@ impl Element for EditorElement { &row_block_types, content_origin, scroll_pixel_position, - edit_prediction_popover_origin, + inline_completion_popover_origin, start_row, end_row, line_height, @@ -9040,7 +8884,7 @@ impl Element for EditorElement { .as_ref() .map(|layout| (layout.bounds, layout.entry.clone())), display_hunks: display_hunks.clone(), - diff_hunk_control_bounds, + diff_hunk_control_bounds: diff_hunk_control_bounds.clone(), }); self.editor.update(cx, |editor, _| { @@ -9075,7 +8919,7 @@ impl Element for EditorElement { cursors, visible_cursors, selections, - edit_prediction_popover, + inline_completion_popover, diff_hunk_controls, mouse_context_menu, test_indicators, @@ -9157,7 +9001,7 @@ impl Element for EditorElement { self.paint_minimap(layout, window, cx); self.paint_scrollbars(layout, window, cx); - self.paint_edit_prediction_popover(layout, window, cx); + self.paint_inline_completion_popover(layout, window, cx); self.paint_mouse_context_menu(layout, window, cx); }); }) @@ -9258,7 +9102,7 @@ pub struct EditorLayout { expand_toggles: Vec)>>, diff_hunk_controls: Vec, crease_trailers: Vec>, - edit_prediction_popover: Option, + inline_completion_popover: Option, mouse_context_menu: Option, tab_invisible: ShapedLine, space_invisible: ShapedLine, @@ -9738,12 +9582,14 @@ impl PointForPosition { false } else if start_row == end_row { candidate_col >= start_col && candidate_col < end_col - } else if candidate_row == start_row { - candidate_col >= start_col - } else if candidate_row == end_row { - candidate_col < end_col } else { - true + if candidate_row == start_row { + candidate_col >= start_col + } else if candidate_row == end_row { + candidate_col < end_col + } else { + true + } } } } @@ -9808,7 +9654,7 @@ pub fn layout_line( let chunks = snapshot.highlighted_chunks(row..row + DisplayRow(1), true, style); LineWithInvisibles::from_chunks( chunks, - style, + &style, MAX_LINE_LEN, 1, &snapshot.mode, @@ -9925,7 +9771,7 @@ impl CursorLayout { .px_0p5() .line_height(text_size + px(2.)) .text_color(cursor_name.color) - .child(cursor_name.string) + .child(cursor_name.string.clone()) .into_any_element(); name_element.prepaint_as_root(name_origin, AvailableSpace::min_size(), window, cx); @@ -10178,10 +10024,10 @@ fn compute_auto_height_layout( let overscroll = size(em_width, px(0.)); let editor_width = text_width - gutter_dimensions.margin - overscroll.width - em_width; - if !matches!(editor.soft_wrap_mode(cx), SoftWrap::None) - && editor.set_wrap_width(Some(editor_width), cx) - { - snapshot = editor.snapshot(window, cx); + if !matches!(editor.soft_wrap_mode(cx), SoftWrap::None) { + if editor.set_wrap_width(Some(editor_width), cx) { + snapshot = editor.snapshot(window, cx); + } } let scroll_height = (snapshot.max_point().row().next_row().0 as f32) * line_height; @@ -10213,71 +10059,6 @@ mod tests { use std::num::NonZeroU32; use util::test::sample_text; - #[gpui::test] - async fn test_soft_wrap_editor_width_auto_height_editor(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let window = cx.add_window(|window, cx| { - let buffer = MultiBuffer::build_simple(&"a ".to_string().repeat(100), cx); - let mut editor = Editor::new( - EditorMode::AutoHeight { - min_lines: 1, - max_lines: None, - }, - buffer, - None, - window, - cx, - ); - editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx); - editor - }); - let cx = &mut VisualTestContext::from_window(*window, cx); - let editor = window.root(cx).unwrap(); - let style = cx.update(|_, cx| editor.read(cx).style().unwrap().clone()); - - for x in 1..=100 { - let (_, state) = cx.draw( - Default::default(), - size(px(200. + 0.13 * x as f32), px(500.)), - |_, _| EditorElement::new(&editor, style.clone()), - ); - - assert!( - state.position_map.scroll_max.x == 0., - "Soft wrapped editor should have no horizontal scrolling!" - ); - } - } - - #[gpui::test] - async fn test_soft_wrap_editor_width_full_editor(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let window = cx.add_window(|window, cx| { - let buffer = MultiBuffer::build_simple(&"a ".to_string().repeat(100), cx); - let mut editor = Editor::new(EditorMode::full(), buffer, None, window, cx); - editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx); - editor - }); - let cx = &mut VisualTestContext::from_window(*window, cx); - let editor = window.root(cx).unwrap(); - let style = cx.update(|_, cx| editor.read(cx).style().unwrap().clone()); - - for x in 1..=100 { - let (_, state) = cx.draw( - Default::default(), - size(px(200. + 0.13 * x as f32), px(500.)), - |_, _| EditorElement::new(&editor, style.clone()), - ); - - assert!( - state.position_map.scroll_max.x == 0., - "Soft wrapped editor should have no horizontal scrolling!" - ); - } - } - #[gpui::test] fn test_shape_line_numbers(cx: &mut TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/editor/src/git/blame.rs b/crates/editor/src/git/blame.rs index b11617ccec..fc350a5a15 100644 --- a/crates/editor/src/git/blame.rs +++ b/crates/editor/src/git/blame.rs @@ -213,8 +213,8 @@ impl GitBlame { let project_subscription = cx.subscribe(&project, { let buffer = buffer.clone(); - move |this, _, event, cx| { - if let project::Event::WorktreeUpdatedEntries(_, updated) = event { + move |this, _, event, cx| match event { + project::Event::WorktreeUpdatedEntries(_, updated) => { let project_entry_id = buffer.read(cx).entry_id(cx); if updated .iter() @@ -224,6 +224,7 @@ impl GitBlame { this.generate(cx); } } + _ => {} } }); @@ -291,7 +292,7 @@ impl GitBlame { let buffer_id = self.buffer_snapshot.remote_id(); let mut cursor = self.entries.cursor::(&()); - rows.iter().map(move |info| { + rows.into_iter().map(move |info| { let row = info .buffer_row .filter(|_| info.buffer_id == Some(buffer_id))?; @@ -311,10 +312,10 @@ impl GitBlame { .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; + if let Some(author_len) = author_len { + if author_len > max_author_length { + max_author_length = author_len; + } } } @@ -414,20 +415,21 @@ impl GitBlame { let old_end = cursor.end(); if row_edits .peek() - .is_none_or(|next_edit| next_edit.old.start >= old_end) - && let Some(entry) = cursor.item() + .map_or(true, |next_edit| next_edit.old.start >= old_end) { - if old_end > edit.old.end { - new_entries.push( - GitBlameEntry { - rows: cursor.end() - edit.old.end, - blame: entry.blame.clone(), - }, - &(), - ); - } + 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(); + cursor.next(); + } } } new_entries.append(cursor.suffix(), &()); diff --git a/crates/editor/src/highlight_matching_bracket.rs b/crates/editor/src/highlight_matching_bracket.rs index aa4e616924..e38197283d 100644 --- a/crates/editor/src/highlight_matching_bracket.rs +++ b/crates/editor/src/highlight_matching_bracket.rs @@ -1,7 +1,6 @@ use crate::{Editor, RangeToAnchorExt}; -use gpui::{Context, HighlightStyle, Window}; +use gpui::{Context, Window}; use language::CursorShape; -use theme::ActiveTheme; enum MatchingBracketHighlight {} @@ -10,7 +9,7 @@ pub fn refresh_matching_bracket_highlights( window: &mut Window, cx: &mut Context, ) { - editor.clear_highlights::(cx); + editor.clear_background_highlights::(cx); let newest_selection = editor.selections.newest::(cx); // Don't highlight brackets if the selection isn't empty @@ -36,19 +35,12 @@ pub fn refresh_matching_bracket_highlights( .buffer_snapshot .innermost_enclosing_bracket_ranges(head..tail, None) { - editor.highlight_text::( - vec![ + editor.highlight_background::( + &[ opening_range.to_anchors(&snapshot.buffer_snapshot), closing_range.to_anchors(&snapshot.buffer_snapshot), ], - HighlightStyle { - background_color: Some( - cx.theme() - .colors() - .editor_document_highlight_bracket_background, - ), - ..Default::default() - }, + |theme| theme.colors().editor_document_highlight_bracket_background, cx, ) } @@ -112,7 +104,7 @@ mod tests { another_test(1, 2, 3); } "#}); - cx.assert_editor_text_highlights::(indoc! {r#" + cx.assert_editor_background_highlights::(indoc! {r#" pub fn test«(»"Test argument"«)» { another_test(1, 2, 3); } @@ -123,7 +115,7 @@ mod tests { another_test(1, ˇ2, 3); } "#}); - cx.assert_editor_text_highlights::(indoc! {r#" + cx.assert_editor_background_highlights::(indoc! {r#" pub fn test("Test argument") { another_test«(»1, 2, 3«)»; } @@ -134,7 +126,7 @@ mod tests { anotherˇ_test(1, 2, 3); } "#}); - cx.assert_editor_text_highlights::(indoc! {r#" + cx.assert_editor_background_highlights::(indoc! {r#" pub fn test("Test argument") «{» another_test(1, 2, 3); «}» @@ -146,7 +138,7 @@ mod tests { another_test(1, 2, 3); } "#}); - cx.assert_editor_text_highlights::(indoc! {r#" + cx.assert_editor_background_highlights::(indoc! {r#" pub fn test("Test argument") { another_test(1, 2, 3); } @@ -158,8 +150,8 @@ mod tests { another_test(1, 2, 3); } "#}); - cx.assert_editor_text_highlights::(indoc! {r#" - pub fn test«("Test argument") { + cx.assert_editor_background_highlights::(indoc! {r#" + pub fn test("Test argument") { another_test(1, 2, 3); } "#}); diff --git a/crates/editor/src/hover_links.rs b/crates/editor/src/hover_links.rs index 94f49f601a..02f93e6829 100644 --- a/crates/editor/src/hover_links.rs +++ b/crates/editor/src/hover_links.rs @@ -271,7 +271,7 @@ impl Editor { Task::ready(Ok(Navigated::No)) }; self.select(SelectPhase::End, window, cx); - navigate_task + return navigate_task; } } @@ -321,10 +321,7 @@ 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) = snapshot - .buffer_snapshot - .buffer_id_for_anchor(previous_valid_anchor) - { + if let Some(buffer_id) = previous_valid_anchor.buffer_id { inlay_hint_cache.spawn_hint_resolve( buffer_id, excerpt_id, @@ -421,22 +418,24 @@ 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() { - go_to_definition_updated = true; - show_link_definition( - shift_held, - editor, - TriggerPoint::InlayHint( - highlight, - location, - language_server_id, - ), - snapshot, - window, - cx, - ); + 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, + ); + } } } } @@ -562,7 +561,7 @@ pub fn show_link_definition( provider.definitions(&buffer, buffer_position, preferred_kind, cx) })?; if let Some(task) = task { - task.await.ok().flatten().map(|definition_result| { + task.await.ok().map(|definition_result| { ( definition_result.iter().find_map(|link| { link.origin.as_ref().and_then(|origin| { @@ -658,11 +657,11 @@ pub fn show_link_definition( pub(crate) fn find_url( buffer: &Entity, position: text::Anchor, - cx: AsyncWindowContext, + mut cx: AsyncWindowContext, ) -> Option<(Range, String)> { const LIMIT: usize = 2048; - let Ok(snapshot) = buffer.read_with(&cx, |buffer, _| buffer.snapshot()) else { + let Ok(snapshot) = buffer.read_with(&mut cx, |buffer, _| buffer.snapshot()) else { return None; }; @@ -720,11 +719,11 @@ pub(crate) fn find_url( pub(crate) fn find_url_from_range( buffer: &Entity, range: Range, - cx: AsyncWindowContext, + mut cx: AsyncWindowContext, ) -> Option { const LIMIT: usize = 2048; - let Ok(snapshot) = buffer.read_with(&cx, |buffer, _| buffer.snapshot()) else { + let Ok(snapshot) = buffer.read_with(&mut cx, |buffer, _| buffer.snapshot()) else { return None; }; @@ -767,11 +766,10 @@ pub(crate) fn find_url_from_range( let mut finder = LinkFinder::new(); finder.kinds(&[LinkKind::Url]); - if let Some(link) = finder.links(&text).next() - && link.start() == 0 - && link.end() == text.len() - { - return Some(link.as_str().to_string()); + if let Some(link) = finder.links(&text).next() { + if link.start() == 0 && link.end() == text.len() { + return Some(link.as_str().to_string()); + } } None @@ -796,7 +794,7 @@ pub(crate) async fn find_file( ) -> Option { 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 @@ -874,7 +872,7 @@ fn surrounding_filename( .peekable(); while let Some(ch) = forwards.next() { // Skip escaped whitespace - if ch == '\\' && forwards.peek().is_some_and(|ch| ch.is_whitespace()) { + if ch == '\\' && forwards.peek().map_or(false, |ch| ch.is_whitespace()) { token_end += ch.len_utf8(); let whitespace = forwards.next().unwrap(); token_end += whitespace.len_utf8(); diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index fab5345787..bda229e346 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -142,11 +142,11 @@ pub fn hover_at_inlay( .info_popovers .iter() .any(|InfoPopover { symbol_range, .. }| { - 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; + 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; + } } false }) @@ -167,16 +167,17 @@ 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, Some(&language_registry), None, cx).await; + let parsed_content = parse_blocks(&blocks, &language_registry, None, cx).await; let scroll_handle = ScrollHandle::new(); let subscription = this .update(cx, |_, cx| { - parsed_content.as_ref().map(|parsed_content| { - cx.observe(parsed_content, |_, _, cx| cx.notify()) - }) + if let Some(parsed_content) = &parsed_content { + Some(cx.observe(parsed_content, |_, _, cx| cx.notify())) + } else { + None + } }) .ok() .flatten(); @@ -250,9 +251,7 @@ fn show_hover( let (excerpt_id, _, _) = editor.buffer().read(cx).excerpt_containing(anchor, cx)?; - let language_registry = editor - .project() - .map(|project| project.read(cx).languages().clone()); + let language_registry = editor.project.as_ref()?.read(cx).languages().clone(); let provider = editor.semantics_provider.clone()?; if !ignore_timeout { @@ -268,12 +267,13 @@ 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 - && triggered_from + if let Some(triggered_from) = &editor.hover_state.triggered_from { + if 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.unwrap_or_default() + hover_request.await } else { Vec::new() }; @@ -443,14 +443,15 @@ fn show_hover( text: format!("Unicode character U+{:02X}", invisible as u32), kind: HoverBlockKind::PlainText, }]; - let parsed_content = - parse_blocks(&blocks, language_registry.as_ref(), None, cx).await; + let parsed_content = parse_blocks(&blocks, &language_registry, None, cx).await; let scroll_handle = ScrollHandle::new(); let subscription = this .update(cx, |_, cx| { - parsed_content.as_ref().map(|parsed_content| { - cx.observe(parsed_content, |_, _, cx| cx.notify()) - }) + if let Some(parsed_content) = &parsed_content { + Some(cx.observe(parsed_content, |_, _, cx| cx.notify())) + } else { + None + } }) .ok() .flatten(); @@ -492,15 +493,16 @@ fn show_hover( let blocks = hover_result.contents; let language = hover_result.language; - let parsed_content = - parse_blocks(&blocks, language_registry.as_ref(), language, cx).await; + let parsed_content = parse_blocks(&blocks, &language_registry, language, cx).await; let scroll_handle = ScrollHandle::new(); hover_highlights.push(range.clone()); let subscription = this .update(cx, |_, cx| { - parsed_content.as_ref().map(|parsed_content| { - cx.observe(parsed_content, |_, _, cx| cx.notify()) - }) + if let Some(parsed_content) = &parsed_content { + Some(cx.observe(parsed_content, |_, _, cx| cx.notify())) + } else { + None + } }) .ok() .flatten(); @@ -581,7 +583,7 @@ fn same_diagnostic_hover(editor: &Editor, snapshot: &EditorSnapshot, anchor: Anc async fn parse_blocks( blocks: &[HoverBlock], - language_registry: Option<&Arc>, + language_registry: &Arc, language: Option>, cx: &mut AsyncWindowContext, ) -> Option> { @@ -597,15 +599,18 @@ async fn parse_blocks( }) .join("\n\n"); - cx.new_window_entity(|_window, cx| { - Markdown::new( - combined_text.into(), - language_registry.cloned(), - language.map(|language| language.name()), - cx, - ) - }) - .ok() + 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 } pub fn hover_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { @@ -617,7 +622,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), + font_family: Some(ui_font_family.clone()), font_fallbacks: ui_font_fallbacks, color: Some(cx.theme().colors().editor_foreground), ..Default::default() @@ -666,7 +671,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), + font_family: Some(ui_font_family.clone()), font_fallbacks: ui_font_fallbacks, color: Some(cx.theme().colors().editor_foreground), ..Default::default() @@ -707,54 +712,59 @@ 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) - && uri.scheme() == "file" - && let Some(workspace) = window.root::().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) { + if uri.scheme() == "file" { + if let Some(workspace) = window.root::().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::(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::(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); } @@ -824,19 +834,20 @@ impl HoverState { pub fn focused(&self, window: &mut Window, cx: &mut Context) -> bool { let mut hover_popover_is_focused = false; for info_popover in &self.info_popovers { - 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(markdown_view) = &info_popover.parsed_content { + if markdown_view.focus_handle(cx).is_focused(window) { + hover_popover_is_focused = true; + } } } - if let Some(diagnostic_popover) = &self.diagnostic_popover - && diagnostic_popover + if let Some(diagnostic_popover) = &self.diagnostic_popover { + if diagnostic_popover .markdown .focus_handle(cx) .is_focused(window) - { - hover_popover_is_focused = true; + { + hover_popover_is_focused = true; + } } hover_popover_is_focused } diff --git a/crates/editor/src/indent_guides.rs b/crates/editor/src/indent_guides.rs index 23717eeb15..f6d51c929a 100644 --- a/crates/editor/src/indent_guides.rs +++ b/crates/editor/src/indent_guides.rs @@ -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::>::new(); - let folds = snapshot.folds_in_range(start_offset..end_offset).peekable(); - for fold in folds { + let mut folds = snapshot.folds_in_range(start_offset..end_offset).peekable(); + while let Some(fold) = folds.next() { 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() - && last_range.end >= start - { - last_range.end = last_range.end.max(end); - continue; + if let Some(last_range) = fold_ranges.last_mut() { + if last_range.end >= start { + last_range.end = last_range.end.max(end); + continue; + } } fold_ranges.push(start..end); } diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index dbf5ac95b7..db01cc7ad1 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -475,7 +475,10 @@ 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) = multi_buffer.buffer_for_anchor(*shown_anchor, cx) else { + let Some(buffer) = shown_anchor + .buffer_id + .and_then(|buffer_id| multi_buffer.buffer(buffer_id)) + else { return false; }; let buffer_snapshot = buffer.read(cx).snapshot(); @@ -495,14 +498,16 @@ impl InlayHintCache { cmp::Ordering::Less | cmp::Ordering::Equal => { if !old_kinds.contains(&cached_hint.kind) && new_kinds.contains(&cached_hint.kind) - && 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, - )); + if 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, + )); + } } excerpt_cache.next(); } @@ -517,16 +522,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) - && let Some(anchor) = multi_buffer_snapshot + if !old_kinds.contains(&cached_hint_kind) && new_kinds.contains(&cached_hint_kind) { + if 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, + )); + } } } } @@ -615,44 +620,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) - && 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 - { - resolved_hint.resolve_state = ResolveState::Resolved; - *cached_hint = resolved_hint; - } - } + 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) + { + 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; + } + } + } + })?; + } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } } } } @@ -985,8 +990,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) - && let Some(project) = editor.project.as_ref() { + if !editor.registered_buffers.contains_key(&query.buffer_id) { + if let Some(project) = editor.project.as_ref() { project.update(cx, |project, cx| { editor.registered_buffers.insert( query.buffer_id, @@ -994,6 +999,7 @@ fn fetch_and_update_hints( ); }) } + } editor .semantics_provider @@ -1234,12 +1240,14 @@ fn apply_hint_update( .inlay_hint_cache .allowed_hint_kinds .contains(&new_hint.kind) - && 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)); + if 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)); + } } let new_id = InlayId::Hint(new_inlay_id); cached_excerpt_hints.hints_by_id.insert(new_id, new_hint); @@ -3538,7 +3546,7 @@ pub mod tests { let excerpt_hints = excerpt_hints.read(); for id in &excerpt_hints.ordered_hints { let hint = &excerpt_hints.hints_by_id[id]; - let mut label = hint.text().to_string(); + let mut label = hint.text(); if hint.padding_left { label.insert(0, ' '); } diff --git a/crates/editor/src/edit_prediction_tests.rs b/crates/editor/src/inline_completion_tests.rs similarity index 52% rename from crates/editor/src/edit_prediction_tests.rs rename to crates/editor/src/inline_completion_tests.rs index bba632e81f..5ac34c94f5 100644 --- a/crates/editor/src/edit_prediction_tests.rs +++ b/crates/editor/src/inline_completion_tests.rs @@ -1,28 +1,26 @@ -use edit_prediction::EditPredictionProvider; use gpui::{Entity, prelude::*}; use indoc::indoc; +use inline_completion::EditPredictionProvider; use multi_buffer::{Anchor, MultiBufferSnapshot, ToPoint}; use project::Project; use std::ops::Range; use text::{Point, ToOffset}; use crate::{ - EditPrediction, - editor_tests::{init_test, update_test_language_settings}, - test::editor_test_context::EditorTestContext, + InlineCompletion, editor_tests::init_test, test::editor_test_context::EditorTestContext, }; #[gpui::test] -async fn test_edit_prediction_insert(cx: &mut gpui::TestAppContext) { +async fn test_inline_completion_insert(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; - let provider = cx.new(|_| FakeEditPredictionProvider::default()); + let provider = cx.new(|_| FakeInlineCompletionProvider::default()); assign_editor_completion_provider(provider.clone(), &mut cx); cx.set_state("let absolute_zero_celsius = ˇ;"); propose_edits(&provider, vec![(28..28, "-273.15")], &mut cx); - cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); + cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx)); assert_editor_active_edit_completion(&mut cx, |_, edits| { assert_eq!(edits.len(), 1); @@ -35,16 +33,16 @@ async fn test_edit_prediction_insert(cx: &mut gpui::TestAppContext) { } #[gpui::test] -async fn test_edit_prediction_modification(cx: &mut gpui::TestAppContext) { +async fn test_inline_completion_modification(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; - let provider = cx.new(|_| FakeEditPredictionProvider::default()); + let provider = cx.new(|_| FakeInlineCompletionProvider::default()); assign_editor_completion_provider(provider.clone(), &mut cx); cx.set_state("let pi = ˇ\"foo\";"); propose_edits(&provider, vec![(9..14, "3.14159")], &mut cx); - cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); + cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx)); assert_editor_active_edit_completion(&mut cx, |_, edits| { assert_eq!(edits.len(), 1); @@ -57,11 +55,11 @@ async fn test_edit_prediction_modification(cx: &mut gpui::TestAppContext) { } #[gpui::test] -async fn test_edit_prediction_jump_button(cx: &mut gpui::TestAppContext) { +async fn test_inline_completion_jump_button(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; - let provider = cx.new(|_| FakeEditPredictionProvider::default()); + let provider = cx.new(|_| FakeInlineCompletionProvider::default()); assign_editor_completion_provider(provider.clone(), &mut cx); // Cursor is 2+ lines above the proposed edit @@ -79,7 +77,7 @@ async fn test_edit_prediction_jump_button(cx: &mut gpui::TestAppContext) { &mut cx, ); - cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); + cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx)); assert_editor_active_move_completion(&mut cx, |snapshot, move_target| { assert_eq!(move_target.to_point(&snapshot), Point::new(4, 3)); }); @@ -109,7 +107,7 @@ async fn test_edit_prediction_jump_button(cx: &mut gpui::TestAppContext) { &mut cx, ); - cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); + cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx)); assert_editor_active_move_completion(&mut cx, |snapshot, move_target| { assert_eq!(move_target.to_point(&snapshot), Point::new(1, 3)); }); @@ -126,11 +124,11 @@ async fn test_edit_prediction_jump_button(cx: &mut gpui::TestAppContext) { } #[gpui::test] -async fn test_edit_prediction_invalidation_range(cx: &mut gpui::TestAppContext) { +async fn test_inline_completion_invalidation_range(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; - let provider = cx.new(|_| FakeEditPredictionProvider::default()); + let provider = cx.new(|_| FakeInlineCompletionProvider::default()); assign_editor_completion_provider(provider.clone(), &mut cx); // Cursor is 3+ lines above the proposed edit @@ -150,7 +148,7 @@ async fn test_edit_prediction_invalidation_range(cx: &mut gpui::TestAppContext) &mut cx, ); - cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); + cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx)); assert_editor_active_move_completion(&mut cx, |snapshot, move_target| { assert_eq!(move_target.to_point(&snapshot), edit_location); }); @@ -178,7 +176,7 @@ async fn test_edit_prediction_invalidation_range(cx: &mut gpui::TestAppContext) line "}); cx.editor(|editor, _, _| { - assert!(editor.active_edit_prediction.is_none()); + assert!(editor.active_inline_completion.is_none()); }); // Cursor is 3+ lines below the proposed edit @@ -198,7 +196,7 @@ async fn test_edit_prediction_invalidation_range(cx: &mut gpui::TestAppContext) &mut cx, ); - cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); + cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx)); assert_editor_active_move_completion(&mut cx, |snapshot, move_target| { assert_eq!(move_target.to_point(&snapshot), edit_location); }); @@ -226,88 +224,7 @@ async fn test_edit_prediction_invalidation_range(cx: &mut gpui::TestAppContext) line ˇ5 "}); cx.editor(|editor, _, _| { - assert!(editor.active_edit_prediction.is_none()); - }); -} - -#[gpui::test] -async fn test_edit_prediction_jump_disabled_for_non_zed_providers(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let mut cx = EditorTestContext::new(cx).await; - let provider = cx.new(|_| FakeNonZedEditPredictionProvider::default()); - assign_editor_completion_provider_non_zed(provider.clone(), &mut cx); - - // Cursor is 2+ lines above the proposed edit - cx.set_state(indoc! {" - line 0 - line ˇ1 - line 2 - line 3 - line - "}); - - propose_edits_non_zed( - &provider, - vec![(Point::new(4, 3)..Point::new(4, 3), " 4")], - &mut cx, - ); - - cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); - - // For non-Zed providers, there should be no move completion (jump functionality disabled) - cx.editor(|editor, _, _| { - if let Some(completion_state) = &editor.active_edit_prediction { - // Should be an Edit prediction, not a Move prediction - match &completion_state.completion { - EditPrediction::Edit { .. } => { - // This is expected for non-Zed providers - } - EditPrediction::Move { .. } => { - panic!( - "Non-Zed providers should not show Move predictions (jump functionality)" - ); - } - } - } - }); -} - -#[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" - ); + assert!(editor.active_inline_completion.is_none()); }); } @@ -317,11 +234,11 @@ fn assert_editor_active_edit_completion( ) { cx.editor(|editor, _, cx| { let completion_state = editor - .active_edit_prediction + .active_inline_completion .as_ref() .expect("editor has no active completion"); - if let EditPrediction::Edit { edits, .. } = &completion_state.completion { + if let InlineCompletion::Edit { edits, .. } = &completion_state.completion { assert(editor.buffer().read(cx).snapshot(cx), edits); } else { panic!("expected edit completion"); @@ -335,11 +252,11 @@ fn assert_editor_active_move_completion( ) { cx.editor(|editor, _, cx| { let completion_state = editor - .active_edit_prediction + .active_inline_completion .as_ref() .expect("editor has no active completion"); - if let EditPrediction::Move { target, .. } = &completion_state.completion { + if let InlineCompletion::Move { target, .. } = &completion_state.completion { assert(editor.buffer().read(cx).snapshot(cx), *target); } else { panic!("expected move completion"); @@ -354,7 +271,7 @@ fn accept_completion(cx: &mut EditorTestContext) { } fn propose_edits( - provider: &Entity, + provider: &Entity, edits: Vec<(Range, &str)>, cx: &mut EditorTestContext, ) { @@ -366,7 +283,7 @@ fn propose_edits( cx.update(|_, cx| { provider.update(cx, |provider, _| { - provider.set_edit_prediction(Some(edit_prediction::EditPrediction { + provider.set_inline_completion(Some(inline_completion::InlineCompletion { id: None, edits: edits.collect(), edit_preview: None, @@ -376,38 +293,7 @@ fn propose_edits( } fn assign_editor_completion_provider( - provider: Entity, - cx: &mut EditorTestContext, -) { - cx.update_editor(|editor, window, cx| { - editor.set_edit_prediction_provider(Some(provider), window, cx); - }) -} - -fn propose_edits_non_zed( - provider: &Entity, - edits: Vec<(Range, &str)>, - cx: &mut EditorTestContext, -) { - let snapshot = cx.buffer_snapshot(); - let edits = edits.into_iter().map(|(range, text)| { - let range = snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end); - (range, text.into()) - }); - - cx.update(|_, cx| { - provider.update(cx, |provider, _| { - provider.set_edit_prediction(Some(edit_prediction::EditPrediction { - id: None, - edits: edits.collect(), - edit_preview: None, - })) - }) - }); -} - -fn assign_editor_completion_provider_non_zed( - provider: Entity, + provider: Entity, cx: &mut EditorTestContext, ) { cx.update_editor(|editor, window, cx| { @@ -416,17 +302,20 @@ fn assign_editor_completion_provider_non_zed( } #[derive(Default, Clone)] -pub struct FakeEditPredictionProvider { - pub completion: Option, +pub struct FakeInlineCompletionProvider { + pub completion: Option, } -impl FakeEditPredictionProvider { - pub fn set_edit_prediction(&mut self, completion: Option) { +impl FakeInlineCompletionProvider { + pub fn set_inline_completion( + &mut self, + completion: Option, + ) { self.completion = completion; } } -impl EditPredictionProvider for FakeEditPredictionProvider { +impl EditPredictionProvider for FakeInlineCompletionProvider { fn name() -> &'static str { "fake-completion-provider" } @@ -439,10 +328,6 @@ impl EditPredictionProvider for FakeEditPredictionProvider { false } - fn supports_jump_to_edit() -> bool { - true - } - fn is_enabled( &self, _buffer: &gpui::Entity, @@ -470,7 +355,7 @@ impl EditPredictionProvider for FakeEditPredictionProvider { &mut self, _buffer: gpui::Entity, _cursor_position: language::Anchor, - _direction: edit_prediction::Direction, + _direction: inline_completion::Direction, _cx: &mut gpui::Context, ) { } @@ -484,81 +369,7 @@ impl EditPredictionProvider for FakeEditPredictionProvider { _buffer: &gpui::Entity, _cursor_position: language::Anchor, _cx: &mut gpui::Context, - ) -> Option { - self.completion.clone() - } -} - -#[derive(Default, Clone)] -pub struct FakeNonZedEditPredictionProvider { - pub completion: Option, -} - -impl FakeNonZedEditPredictionProvider { - pub fn set_edit_prediction(&mut self, completion: Option) { - self.completion = completion; - } -} - -impl EditPredictionProvider for FakeNonZedEditPredictionProvider { - fn name() -> &'static str { - "fake-non-zed-provider" - } - - fn display_name() -> &'static str { - "Fake Non-Zed Provider" - } - - fn show_completions_in_menu() -> bool { - false - } - - fn supports_jump_to_edit() -> bool { - false - } - - fn is_enabled( - &self, - _buffer: &gpui::Entity, - _cursor_position: language::Anchor, - _cx: &gpui::App, - ) -> bool { - true - } - - fn is_refreshing(&self) -> bool { - false - } - - fn refresh( - &mut self, - _project: Option>, - _buffer: gpui::Entity, - _cursor_position: language::Anchor, - _debounce: bool, - _cx: &mut gpui::Context, - ) { - } - - fn cycle( - &mut self, - _buffer: gpui::Entity, - _cursor_position: language::Anchor, - _direction: edit_prediction::Direction, - _cx: &mut gpui::Context, - ) { - } - - fn accept(&mut self, _cx: &mut gpui::Context) {} - - fn discard(&mut self, _cx: &mut gpui::Context) {} - - fn suggest<'a>( - &mut self, - _buffer: &gpui::Entity, - _cursor_position: language::Anchor, - _cx: &mut gpui::Context, - ) -> Option { + ) -> Option { self.completion.clone() } } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index b7110190fd..ca635a2132 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1,7 +1,7 @@ use crate::{ Anchor, Autoscroll, Editor, EditorEvent, EditorSettings, ExcerptId, ExcerptRange, FormatTarget, - MultiBuffer, MultiBufferSnapshot, NavigationData, ReportEditorEvent, SearchWithinRange, - SelectionEffects, ToPoint as _, + MultiBuffer, MultiBufferSnapshot, NavigationData, SearchWithinRange, SelectionEffects, + ToPoint as _, display_map::HighlightKey, editor_settings::SeedQuerySetting, persistence::{DB, SerializedEditor}, @@ -42,7 +42,6 @@ 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}, }; @@ -104,9 +103,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 sorted_excerpts = sorted_excerpts.into_iter().peekable(); + let mut sorted_excerpts = sorted_excerpts.into_iter().peekable(); - for excerpt in sorted_excerpts { + while let Some(excerpt) = sorted_excerpts.next() { let Ok(buffer_id) = BufferId::new(excerpt.buffer_id) else { continue; }; @@ -202,7 +201,7 @@ impl FollowableItem for Editor { if buffer .as_singleton() .and_then(|buffer| buffer.read(cx).file()) - .is_some_and(|file| file.is_private()) + .map_or(false, |file| file.is_private()) { return None; } @@ -294,7 +293,7 @@ impl FollowableItem for Editor { EditorEvent::ExcerptsRemoved { ids, .. } => { update .deleted_excerpts - .extend(ids.iter().copied().map(ExcerptId::to_proto)); + .extend(ids.iter().map(ExcerptId::to_proto)); true } EditorEvent::ScrollPositionChanged { autoscroll, .. } if !autoscroll => { @@ -525,8 +524,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, } } @@ -655,10 +654,6 @@ impl Item for Editor { } } - fn suggested_filename(&self, cx: &App) -> SharedString { - self.buffer.read(cx).title(cx).to_string().into() - } - fn tab_icon(&self, _: &Window, cx: &App) -> Option { ItemSettings::get_global(cx) .file_icons @@ -679,7 +674,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()?.read(cx); + let project = self.project.as_ref()?.read(cx); let entry = project.entry_for_path(&path, cx)?; let (repo, repo_path) = project .git_store() @@ -716,7 +711,7 @@ impl Item for Editor { .read(cx) .as_singleton() .and_then(|buffer| buffer.read(cx).file()) - .is_some_and(|file| file.disk_state() == DiskState::Deleted); + .map_or(false, |file| file.disk_state() == DiskState::Deleted); h_flex() .gap_2() @@ -781,10 +776,6 @@ impl Item for Editor { } } - fn on_removed(&self, cx: &App) { - self.report_editor_event(ReportEditorEvent::Closed, None, cx); - } - fn deactivated(&mut self, _: &mut Window, cx: &mut Context) { let selection = self.selections.newest_anchor(); self.push_to_nav_history(selection.head(), None, true, false, cx); @@ -824,9 +815,9 @@ impl Item for Editor { ) -> Task> { // Add meta data tracking # of auto saves if options.autosave { - self.report_editor_event(ReportEditorEvent::Saved { auto_saved: true }, None, cx); + self.report_editor_event("Editor Autosaved", None, cx); } else { - self.report_editor_event(ReportEditorEvent::Saved { auto_saved: false }, None, cx); + self.report_editor_event("Editor Saved", None, cx); } let buffers = self.buffer().clone().read(cx).all_buffers(); @@ -905,11 +896,7 @@ impl Item for Editor { .path .extension() .map(|a| a.to_string_lossy().to_string()); - self.report_editor_event( - ReportEditorEvent::Saved { auto_saved: false }, - file_extension, - cx, - ); + self.report_editor_event("Editor Saved", file_extension, cx); project.update(cx, |project, cx| project.save_buffer_as(buffer, path, cx)) } @@ -931,10 +918,10 @@ impl Item for Editor { })?; buffer .update(cx, |buffer, cx| { - if let Some(transaction) = transaction - && !buffer.is_singleton() - { - buffer.push_transaction(&transaction.0, cx); + if let Some(transaction) = transaction { + if !buffer.is_singleton() { + buffer.push_transaction(&transaction.0, cx); + } } }) .ok(); @@ -1010,8 +997,8 @@ 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| { - if let workspace::Event::ModalOpened = event { + cx.subscribe(&workspace, |editor, _, event: &workspace::Event, _cx| { + if matches!(event, workspace::Event::ModalOpened) { editor.mouse_context_menu.take(); editor.inline_blame_popover.take(); } @@ -1037,10 +1024,6 @@ impl Item for Editor { f(ItemEvent::UpdateBreadcrumbs); } - EditorEvent::BreadcrumbsChanged => { - f(ItemEvent::UpdateBreadcrumbs); - } - EditorEvent::DirtyChanged => { f(ItemEvent::UpdateTab); } @@ -1293,7 +1276,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)?; @@ -1371,47 +1354,40 @@ 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::()) - .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)); - }); + 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::()) + .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); + } } - 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 { - Some(InvalidBufferView::new(abs_path, is_local, e, window, cx)) - } } fn clip_ranges<'a>( @@ -1849,7 +1825,7 @@ pub fn entry_diagnostic_aware_icon_name_and_color( diagnostic_severity: Option, ) -> Option<(IconName, Color)> { match diagnostic_severity { - Some(DiagnosticSeverity::ERROR) => Some((IconName::Close, Color::Error)), + Some(DiagnosticSeverity::ERROR) => Some((IconName::X, Color::Error)), Some(DiagnosticSeverity::WARNING) => Some((IconName::Triangle, Color::Warning)), _ => None, } diff --git a/crates/editor/src/jsx_tag_auto_close.rs b/crates/editor/src/jsx_tag_auto_close.rs index e6c518beae..95a7925839 100644 --- a/crates/editor/src/jsx_tag_auto_close.rs +++ b/crates/editor/src/jsx_tag_auto_close.rs @@ -37,7 +37,7 @@ pub(crate) fn should_auto_close( let text = buffer .text_for_range(edited_range.clone()) .collect::(); - let edited_range = edited_range.to_offset(buffer); + let edited_range = edited_range.to_offset(&buffer); if !text.ends_with(">") { continue; } @@ -51,11 +51,12 @@ pub(crate) fn should_auto_close( continue; }; let mut jsx_open_tag_node = node; - 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 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 jsx_open_tag_node.grammar_name() != config.open_tag_node_name { continue; @@ -86,9 +87,9 @@ pub(crate) fn should_auto_close( }); } if to_auto_edit.is_empty() { - None + return None; } else { - Some(to_auto_edit) + return Some(to_auto_edit); } } @@ -181,12 +182,12 @@ pub(crate) fn generate_auto_close_edits( */ { let tag_node_name_equals = |node: &Node, name: &str| { - let is_empty = name.is_empty(); + let is_empty = name.len() == 0; 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); } - is_empty + return is_empty; }; let tree_root_node = { @@ -207,7 +208,7 @@ pub(crate) fn generate_auto_close_edits( cur = descendant; } - assert!(!ancestors.is_empty()); + assert!(ancestors.len() > 0); let mut tree_root_node = open_tag; @@ -227,7 +228,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) - .is_some_and(|element_open_tag_node| { + .map_or(false, |element_open_tag_node| { tag_node_name_equals(&element_open_tag_node, &tag_name) }); if has_open_tag_with_same_tag_name { @@ -263,7 +264,8 @@ pub(crate) fn generate_auto_close_edits( } let is_after_open_tag = |node: &Node| { - node.start_byte() < open_tag.start_byte() && node.end_byte() < open_tag.start_byte() + return node.start_byte() < open_tag.start_byte() + && node.end_byte() < open_tag.start_byte(); }; // perf: use cursor for more efficient traversal @@ -282,8 +284,10 @@ 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) && !is_after_open_tag(&node) { - unclosed_open_tag_count -= 1; + if tag_node_name_equals(&node, &tag_name) { + if !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 @@ -300,7 +304,7 @@ pub(crate) fn generate_auto_close_edits( let edit_range = edit_anchor..edit_anchor; edits.push((edit_range, format!("", tag_name))); } - Ok(edits) + return Ok(edits); } pub(crate) fn refresh_enabled_in_any_buffer( @@ -366,7 +370,7 @@ pub(crate) fn construct_initial_buffer_versions_map< initial_buffer_versions.insert(buffer_id, buffer_version); } } - initial_buffer_versions + return initial_buffer_versions; } pub(crate) fn handle_from( @@ -454,9 +458,12 @@ 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).is_none_or(|buffer| { - buffer.read(cx).has_edits_since(&buffer_version_initial) - }) + this.buffer + .read(cx) + .buffer(buffer_id) + .map_or(true, |buffer| { + buffer.read(cx).has_edits_since(&buffer_version_initial) + }) }) .ok()?; @@ -507,7 +514,7 @@ pub(crate) fn handle_from( { let selections = this - .read_with(cx, |this, _| this.selections.disjoint_anchors()) + .read_with(cx, |this, _| this.selections.disjoint_anchors().clone()) .ok()?; for selection in selections.iter() { let Some(selection_buffer_offset_head) = @@ -808,7 +815,10 @@ mod jsx_tag_autoclose_tests { ); buf }); - let buffer_c = cx.new(|cx| language::Buffer::local("( editor: &Editor, cx: &mut App, filter_language: F, - language_server_name: LanguageServerName, -) -> Option<(Anchor, Arc, LanguageServerId, Entity)> + language_server_name: &str, +) -> Task, LanguageServerId, Entity)>> where F: Fn(&Language) -> bool, { - let project = editor.project.clone()?; - editor + let Some(project) = &editor.project else { + return Task::ready(None); + }; + + let applicable_buffers = editor .selections .disjoint_anchors() .iter() .filter_map(|selection| Some((selection.head(), selection.head().buffer_id?))) .unique_by(|(_, buffer_id)| *buffer_id) - .find_map(|(trigger_anchor, buffer_id)| { + .filter_map(|(trigger_anchor, buffer_id)| { let buffer = editor.buffer().read(cx).buffer(buffer_id)?; let language = buffer.read(cx).language_at(trigger_anchor.text_anchor)?; if filter_language(&language) { - let server_id = buffer.update(cx, |buffer, cx| { - project - .read(cx) - .language_server_id_for_name(buffer, &language_server_name, cx) - })?; - Some((trigger_anchor, language, server_id, buffer)) + Some((trigger_anchor, buffer, language)) } else { None } }) + .collect::>(); + + let applicable_buffer_tasks = applicable_buffers + .into_iter() + .map(|(trigger_anchor, buffer, language)| { + let task = buffer.update(cx, |buffer, cx| { + project.update(cx, |project, cx| { + project.language_server_id_for_name(buffer, language_server_name, cx) + }) + }); + (trigger_anchor, buffer, language, task) + }) + .collect::>(); + cx.background_spawn(async move { + for (trigger_anchor, buffer, language, task) in applicable_buffer_tasks { + if let Some(server_id) = task.await { + return Some((trigger_anchor, language, server_id, buffer)); + } + } + + None + }) } async fn lsp_task_context( @@ -76,7 +98,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; @@ -94,9 +116,9 @@ pub fn lsp_tasks( for_position: Option, cx: &mut App, ) -> Task, ResolvedTask)>)>> { - let lsp_task_sources = task_sources + let mut lsp_task_sources = task_sources .iter() - .filter_map(|(name, buffer_ids)| { + .map(|(name, buffer_ids)| { let buffers = buffer_ids .iter() .filter(|&&buffer_id| match for_position { @@ -105,62 +127,61 @@ pub fn lsp_tasks( }) .filter_map(|&buffer_id| project.read(cx).buffer_for_id(buffer_id, cx)) .collect::>(); - - let server_id = buffers.iter().find_map(|buffer| { - project.read_with(cx, |project, cx| { - project.language_server_id_for_name(buffer.read(cx), name, cx) - }) - }); - server_id.zip(Some(buffers)) + language_server_for_buffers(project.clone(), name.clone(), buffers, cx) }) - .collect::>(); + .collect::>(); cx.spawn(async move |cx| { cx.spawn(async move |cx| { let mut lsp_tasks = HashMap::default(); - for (server_id, buffers) in lsp_task_sources { - let mut new_lsp_tasks = Vec::new(); - for buffer in buffers { - let source_kind = match buffer.update(cx, |buffer, _| { - buffer.language().map(|language| language.name()) - }) { - Ok(Some(language_name)) => TaskSourceKind::Lsp { - server: server_id, - language_name: SharedString::from(language_name), - }, - Ok(None) => continue, - Err(_) => return Vec::new(), - }; - let id_base = source_kind.to_id_base(); - let lsp_buffer_context = lsp_task_context(&project, &buffer, cx) - .await - .unwrap_or_default(); + while let Some(server_to_query) = lsp_task_sources.next().await { + if let Some((server_id, buffers)) = server_to_query { + let mut new_lsp_tasks = Vec::new(); + for buffer in buffers { + let source_kind = match buffer.update(cx, |buffer, _| { + buffer.language().map(|language| language.name()) + }) { + Ok(Some(language_name)) => TaskSourceKind::Lsp { + server: server_id, + language_name: SharedString::from(language_name), + }, + Ok(None) => continue, + Err(_) => return Vec::new(), + }; + let id_base = source_kind.to_id_base(); + let lsp_buffer_context = lsp_task_context(&project, &buffer, cx) + .await + .unwrap_or_default(); - if let Ok(runnables_task) = project.update(cx, |project, cx| { - let buffer_id = buffer.read(cx).remote_id(); - project.request_lsp( - buffer, - LanguageServerToQuery::Other(server_id), - GetLspRunnables { - buffer_id, - position: for_position, - }, - cx, - ) - }) && 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)) - }, - )); + if let Ok(runnables_task) = project.update(cx, |project, cx| { + let buffer_id = buffer.read(cx).remote_id(); + project.request_lsp( + buffer, + LanguageServerToQuery::Other(server_id), + GetLspRunnables { + buffer_id, + position: for_position, + }, + 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)) + }, + ), + ); + } + } + lsp_tasks + .entry(source_kind) + .or_insert_with(Vec::new) + .append(&mut new_lsp_tasks); } - lsp_tasks - .entry(source_kind) - .or_insert_with(Vec::new) - .append(&mut new_lsp_tasks); } } lsp_tasks.into_iter().collect() @@ -177,3 +198,27 @@ pub fn lsp_tasks( .await }) } + +fn language_server_for_buffers( + project: Entity, + name: LanguageServerName, + candidates: Vec>, + cx: &mut App, +) -> Task>)>> { + cx.spawn(async move |cx| { + for buffer in &candidates { + let server_id = buffer + .update(cx, |buffer, cx| { + project.update(cx, |project, cx| { + project.language_server_id_for_name(buffer, &name.0, cx) + }) + }) + .ok()? + .await; + if let Some(server_id) = server_id { + return Some((server_id, candidates)); + } + } + None + }) +} diff --git a/crates/editor/src/mouse_context_menu.rs b/crates/editor/src/mouse_context_menu.rs index 3bc334c54c..cbb6791a2f 100644 --- a/crates/editor/src/mouse_context_menu.rs +++ b/crates/editor/src/mouse_context_menu.rs @@ -1,8 +1,8 @@ use crate::{ Copy, CopyAndTrim, CopyPermalinkToLine, Cut, DisplayPoint, DisplaySnapshot, Editor, EvaluateSelectedText, FindAllReferences, GoToDeclaration, GoToDefinition, GoToImplementation, - GoToTypeDefinition, Paste, Rename, RevealInFileManager, RunToCursor, SelectMode, - SelectionEffects, SelectionExt, ToDisplayPoint, ToggleCodeActions, + GoToTypeDefinition, Paste, Rename, RevealInFileManager, SelectMode, SelectionEffects, + SelectionExt, ToDisplayPoint, ToggleCodeActions, actions::{Format, FormatSelections}, selections_collection::SelectionsCollection, }; @@ -61,13 +61,13 @@ impl MouseContextMenu { source, offset: position - (source_position + content_origin), }; - Some(MouseContextMenu::new( + return 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,33 +190,25 @@ pub fn deploy_context_menu( .all::(cx) .into_iter() .any(|s| !s.is_empty()); - 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 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 evaluate_selection = window.is_action_available(&EvaluateSelectedText, cx); - let run_to_cursor = window.is_action_available(&RunToCursor, cx); ui::ContextMenu::build(window, cx, |menu, _window, _cx| { let builder = menu .on_blur_subscription(Subscription::new(|| {})) - .when(run_to_cursor, |builder| { - builder.action("Run to Cursor", Box::new(RunToCursor)) - }) .when(evaluate_selection && has_selections, |builder| { - builder.action("Evaluate Selection", Box::new(EvaluateSelectedText)) + builder + .action("Evaluate Selection", Box::new(EvaluateSelectedText)) + .separator() }) - .when( - run_to_cursor || (evaluate_selection && has_selections), - |builder| builder.separator(), - ) .action("Go to Definition", Box::new(GoToDefinition)) .action("Go to Declaration", Box::new(GoToDeclaration)) .action("Go to Type Definition", Box::new(GoToTypeDefinition)) diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 7a008e3ba2..b9b7cb2e58 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -230,7 +230,7 @@ pub fn indented_line_beginning( if stop_at_soft_boundaries && soft_line_start > indent_start && display_point != soft_line_start { soft_line_start - } else if stop_at_indent && (display_point > indent_start || display_point == line_start) { + } else if stop_at_indent && display_point != indent_start { indent_start } else { line_start @@ -439,17 +439,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 +467,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 +476,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 +485,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 +510,10 @@ pub fn find_preceding_boundary_point( if find_range == FindRange::SingleLine && ch == '\n' { break; } - if let Some(prev_ch) = prev_ch - && is_boundary(ch, prev_ch) - { - break; + if let Some(prev_ch) = prev_ch { + if is_boundary(ch, prev_ch) { + break; + } } offset -= ch.len_utf8(); @@ -562,13 +562,13 @@ pub fn find_boundary_point( if find_range == FindRange::SingleLine && ch == '\n' { 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; + 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; + } } } prev_offset = offset; @@ -603,13 +603,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 - && is_boundary(prev_ch, ch) - { - if start_offset == offset { - trail_offset = Some(offset); - } else { - break; + if let Some(prev_ch) = prev_ch { + if is_boundary(prev_ch, ch) { + if start_offset == offset { + trail_offset = Some(offset); + } else { + break; + } } } offset -= ch.len_utf8(); @@ -651,13 +651,13 @@ pub fn find_boundary_trail( // Find the boundary let start_offset = offset; for ch in forward { - if let Some(prev_ch) = prev_ch - && is_boundary(prev_ch, ch) - { - if start_offset == offset { - trail_offset = Some(offset); - } else { - break; + if let Some(prev_ch) = prev_ch { + if is_boundary(prev_ch, ch) { + if start_offset == offset { + trail_offset = Some(offset); + } else { + break; + } } } offset += ch.len_utf8(); @@ -907,12 +907,12 @@ mod tests { let inlays = (0..buffer_snapshot.len()) .flat_map(|offset| { [ - Inlay::edit_prediction( + Inlay::inline_completion( post_inc(&mut id), buffer_snapshot.anchor_at(offset, Bias::Left), "test", ), - Inlay::edit_prediction( + Inlay::inline_completion( post_inc(&mut id), buffer_snapshot.anchor_at(offset, Bias::Right), "test", diff --git a/crates/editor/src/persistence.rs b/crates/editor/src/persistence.rs index ec7c149b4e..88fde53947 100644 --- a/crates/editor/src/persistence.rs +++ b/crates/editor/src/persistence.rs @@ -1,17 +1,13 @@ use anyhow::Result; -use db::{ - query, - sqlez::{ - bindable::{Bind, Column, StaticColumnCount}, - domain::Domain, - statement::Statement, - }, - sqlez_macros::sql, -}; +use db::sqlez::bindable::{Bind, Column, StaticColumnCount}; +use db::sqlez::statement::Statement; 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)] @@ -87,11 +83,7 @@ impl Column for SerializedEditor { } } -pub struct EditorDb(db::sqlez::thread_safe_connection::ThreadSafeConnection); - -impl Domain for EditorDb { - const NAME: &str = stringify!(EditorDb); - +define_connection!( // Current schema shape using pseudo-rust syntax: // editors( // item_id: usize, @@ -121,8 +113,7 @@ impl Domain for EditorDb { // start: usize, // end: usize, // ) - - const MIGRATIONS: &[&str] = &[ + pub static ref DB: EditorDb = &[ sql! ( CREATE TABLE editors( item_id INTEGER NOT NULL, @@ -198,9 +189,7 @@ impl Domain for EditorDb { ) 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, diff --git a/crates/editor/src/proposed_changes_editor.rs b/crates/editor/src/proposed_changes_editor.rs index 2d4710a8d4..1ead45b3de 100644 --- a/crates/editor/src/proposed_changes_editor.rs +++ b/crates/editor/src/proposed_changes_editor.rs @@ -241,13 +241,24 @@ impl ProposedChangesEditor { event: &BufferEvent, _cx: &mut Context, ) { - if let BufferEvent::Operation { .. } = event { - self.recalculate_diffs_tx - .unbounded_send(RecalculateDiff { - buffer, - debounce: true, - }) - .ok(); + 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(); + // } + _ => (), } } } @@ -431,7 +442,7 @@ impl SemanticsProvider for BranchBufferSemanticsProvider { buffer: &Entity, position: text::Anchor, cx: &mut App, - ) -> Option>>> { + ) -> Option>> { let buffer = self.to_base(buffer, &[position], cx)?; self.0.hover(&buffer, position, cx) } @@ -467,7 +478,7 @@ impl SemanticsProvider for BranchBufferSemanticsProvider { } fn supports_inlay_hints(&self, buffer: &Entity, 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 @@ -480,7 +491,7 @@ impl SemanticsProvider for BranchBufferSemanticsProvider { position: text::Anchor, cx: &mut App, ) -> Option>>> { - let buffer = self.to_base(buffer, &[position], cx)?; + let buffer = self.to_base(&buffer, &[position], cx)?; self.0.document_highlights(&buffer, position, cx) } @@ -490,8 +501,8 @@ impl SemanticsProvider for BranchBufferSemanticsProvider { position: text::Anchor, kind: crate::GotoDefinitionKind, cx: &mut App, - ) -> Option>>>> { - let buffer = self.to_base(buffer, &[position], cx)?; + ) -> Option>>> { + let buffer = self.to_base(&buffer, &[position], cx)?; self.0.definitions(&buffer, position, kind, cx) } diff --git a/crates/editor/src/rust_analyzer_ext.rs b/crates/editor/src/rust_analyzer_ext.rs index cf74ee0a9e..da0f11036f 100644 --- a/crates/editor/src/rust_analyzer_ext.rs +++ b/crates/editor/src/rust_analyzer_ext.rs @@ -26,17 +26,6 @@ fn is_rust_language(language: &Language) -> bool { } pub fn apply_related_actions(editor: &Entity, 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() @@ -46,9 +35,12 @@ pub fn apply_related_actions(editor: &Entity, 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, 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); } } @@ -65,21 +57,21 @@ pub fn go_to_parent_module( return; }; - let Some((trigger_anchor, _, server_to_query, buffer)) = - find_specific_language_server_in_selection( - editor, - cx, - is_rust_language, - RUST_ANALYZER_NAME, - ) - else { - return; - }; + let server_lookup = find_specific_language_server_in_selection( + editor, + cx, + is_rust_language, + RUST_ANALYZER_NAME, + ); let project = project.clone(); let lsp_store = project.read(cx).lsp_store(); let upstream_client = lsp_store.read(cx).upstream_client(); cx.spawn_in(window, async move |editor, cx| { + let Some((trigger_anchor, _, server_to_query, buffer)) = server_lookup.await else { + return anyhow::Ok(()); + }; + let location_links = if let Some((client, project_id)) = upstream_client { let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id())?; @@ -129,7 +121,7 @@ pub fn go_to_parent_module( ) })? .await?; - anyhow::Ok(()) + Ok(()) }) .detach_and_log_err(cx); } @@ -147,19 +139,21 @@ pub fn expand_macro_recursively( return; }; - let Some((trigger_anchor, rust_language, server_to_query, buffer)) = - find_specific_language_server_in_selection( - editor, - cx, - is_rust_language, - RUST_ANALYZER_NAME, - ) - else { - return; - }; + let server_lookup = find_specific_language_server_in_selection( + editor, + cx, + is_rust_language, + RUST_ANALYZER_NAME, + ); + let project = project.clone(); let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client(); cx.spawn_in(window, async move |_editor, cx| { + let Some((trigger_anchor, rust_language, server_to_query, buffer)) = server_lookup.await + else { + return Ok(()); + }; + let macro_expansion = if let Some((client, project_id)) = upstream_client { let buffer_id = buffer.update(cx, |buffer, _| buffer.remote_id())?; let request = proto::LspExtExpandMacro { @@ -237,20 +231,20 @@ pub fn open_docs(editor: &mut Editor, _: &OpenDocs, window: &mut Window, cx: &mu return; }; - let Some((trigger_anchor, _, server_to_query, buffer)) = - find_specific_language_server_in_selection( - editor, - cx, - is_rust_language, - RUST_ANALYZER_NAME, - ) - else { - return; - }; + let server_lookup = find_specific_language_server_in_selection( + editor, + cx, + is_rust_language, + RUST_ANALYZER_NAME, + ); let project = project.clone(); let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client(); cx.spawn_in(window, async move |_editor, cx| { + let Some((trigger_anchor, _, server_to_query, buffer)) = server_lookup.await else { + return Ok(()); + }; + let docs_urls = if let Some((client, project_id)) = upstream_client { let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id())?; let request = proto::LspExtOpenDocs { @@ -293,11 +287,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 - && fs::metadata(Path::new(&local_url[8..])).is_ok() - { - cx.open_url(&local_url); - return; + 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(web_url) = docs_urls.web { @@ -317,7 +311,7 @@ fn cancel_flycheck_action( let Some(project) = &editor.project else { return; }; - let buffer_id = editor + let Some(buffer_id) = editor .selections .disjoint_anchors() .iter() @@ -329,7 +323,10 @@ 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); } @@ -342,7 +339,7 @@ fn run_flycheck_action( let Some(project) = &editor.project else { return; }; - let buffer_id = editor + let Some(buffer_id) = editor .selections .disjoint_anchors() .iter() @@ -354,7 +351,10 @@ 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); } @@ -367,7 +367,7 @@ fn clear_flycheck_action( let Some(project) = &editor.project else { return; }; - let buffer_id = editor + let Some(buffer_id) = editor .selections .disjoint_anchors() .iter() @@ -379,6 +379,9 @@ 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); } diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index 8231448618..ecaf7c11e4 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -348,8 +348,8 @@ impl ScrollManager { self.show_scrollbars } - pub fn take_autoscroll_request(&mut self) -> Option<(Autoscroll, bool)> { - self.autoscroll_request.take() + pub fn autoscroll_request(&self) -> Option { + self.autoscroll_request.map(|(autoscroll, _)| autoscroll) } pub fn active_scrollbar_state(&self) -> Option<&ActiveScrollbarState> { @@ -675,7 +675,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - 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 - ) && (settings.defaults.preferred_line_length as f32) < visible_column_count - { - visible_column_count = settings.defaults.preferred_line_length as f32; + ) { + if (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. - && 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. { + if 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,10 +749,12 @@ 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 { - return Ordering::Equal; + 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; + } } Ordering::Greater diff --git a/crates/editor/src/scroll/actions.rs b/crates/editor/src/scroll/actions.rs index f8104665f9..72827b2fee 100644 --- a/crates/editor/src/scroll/actions.rs +++ b/crates/editor/src/scroll/actions.rs @@ -16,7 +16,7 @@ impl Editor { return; } - if matches!(self.mode, EditorMode::SingleLine) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { cx.propagate(); return; } diff --git a/crates/editor/src/scroll/autoscroll.rs b/crates/editor/src/scroll/autoscroll.rs index 057d622903..e8a1f8da73 100644 --- a/crates/editor/src/scroll/autoscroll.rs +++ b/crates/editor/src/scroll/autoscroll.rs @@ -102,12 +102,15 @@ impl AutoscrollStrategy { pub(crate) struct NeedsHorizontalAutoscroll(pub(crate) bool); impl Editor { + pub fn autoscroll_request(&self) -> Option { + self.scroll_manager.autoscroll_request() + } + pub(crate) fn autoscroll_vertically( &mut self, bounds: Bounds, line_height: Pixels, max_scroll_top: f32, - autoscroll_request: Option<(Autoscroll, bool)>, window: &mut Window, cx: &mut Context, ) -> (NeedsHorizontalAutoscroll, WasScrolled) { @@ -116,12 +119,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() - && 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() { + 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 scroll_position.y > max_scroll_top { @@ -134,7 +137,7 @@ impl Editor { WasScrolled(false) }; - let Some((autoscroll, local)) = autoscroll_request else { + let Some((autoscroll, local)) = self.scroll_manager.autoscroll_request.take() else { return (NeedsHorizontalAutoscroll(false), editor_was_scrolled); }; @@ -281,12 +284,9 @@ impl Editor { scroll_width: Pixels, em_advance: Pixels, layouts: &[LineWithInvisibles], - autoscroll_request: Option<(Autoscroll, bool)>, window: &mut Window, cx: &mut Context, ) -> Option> { - let (_, local) = autoscroll_request?; - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let selections = self.selections.all::(cx); let mut scroll_position = self.scroll_manager.scroll_position(&display_map); @@ -335,10 +335,10 @@ impl Editor { let was_scrolled = if target_left < scroll_left { scroll_position.x = target_left / em_advance; - self.set_scroll_position_internal(scroll_position, local, true, window, cx) + self.set_scroll_position_internal(scroll_position, true, true, window, cx) } else if target_right > scroll_right { scroll_position.x = (target_right - viewport_width) / em_advance; - self.set_scroll_position_internal(scroll_position, local, true, window, cx) + self.set_scroll_position_internal(scroll_position, true, true, window, cx) } else { WasScrolled(false) }; diff --git a/crates/editor/src/scroll/scroll_amount.rs b/crates/editor/src/scroll/scroll_amount.rs index 5992c9023c..b2af4f8e4f 100644 --- a/crates/editor/src/scroll/scroll_amount.rs +++ b/crates/editor/src/scroll/scroll_amount.rs @@ -67,7 +67,10 @@ impl ScrollAmount { } pub fn is_full_page(&self) -> bool { - matches!(self, ScrollAmount::Page(count) if count.abs() == 1.0) + match self { + ScrollAmount::Page(count) if count.abs() == 1.0 => true, + _ => false, + } } pub fn direction(&self) -> ScrollDirection { diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index 0a02390b64..73c5f1c076 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -119,8 +119,8 @@ impl SelectionsCollection { cx: &mut App, ) -> Option> { let map = self.display_map(cx); - - resolve_selections(self.pending_anchor().as_ref(), &map).next() + let selection = resolve_selections(self.pending_anchor().as_ref(), &map).next(); + selection } pub(crate) fn pending_mode(&self) -> Option { @@ -276,18 +276,18 @@ impl SelectionsCollection { cx: &mut App, ) -> Selection { let map = self.display_map(cx); - - resolve_selections([self.newest_anchor()], &map) + let selection = resolve_selections([self.newest_anchor()], &map) .next() - .unwrap() + .unwrap(); + selection } pub fn newest_display(&self, cx: &mut App) -> Selection { let map = self.display_map(cx); - - resolve_selections_display([self.newest_anchor()], &map) + let selection = resolve_selections_display([self.newest_anchor()], &map) .next() - .unwrap() + .unwrap(); + selection } pub fn oldest_anchor(&self) -> &Selection { @@ -303,10 +303,10 @@ impl SelectionsCollection { cx: &mut App, ) -> Selection { let map = self.display_map(cx); - - resolve_selections([self.oldest_anchor()], &map) + let selection = resolve_selections([self.oldest_anchor()], &map) .next() - .unwrap() + .unwrap(); + selection } pub fn first_anchor(&self) -> Selection { diff --git a/crates/editor/src/signature_help.rs b/crates/editor/src/signature_help.rs index cb21f35d7e..3447e66ccd 100644 --- a/crates/editor/src/signature_help.rs +++ b/crates/editor/src/signature_help.rs @@ -169,7 +169,7 @@ impl Editor { else { return; }; - let Some(lsp_store) = self.project().map(|p| p.read(cx).lsp_store()) else { + let Some(lsp_store) = self.project.as_ref().map(|p| p.read(cx).lsp_store()) else { return; }; let task = lsp_store.update(cx, |lsp_store, cx| { @@ -182,9 +182,7 @@ impl Editor { let signature_help = task.await; editor .update(cx, |editor, cx| { - let Some(mut signature_help) = - signature_help.unwrap_or_default().into_iter().next() - else { + let Some(mut signature_help) = signature_help.into_iter().next() else { editor .signature_help_state .hide(SignatureHelpHiddenBy::AutoClose); @@ -193,12 +191,12 @@ impl Editor { if let Some(language) = language { for signature in &mut signature_help.signatures { - let text = Rope::from(signature.label.as_ref()); + let text = Rope::from(signature.label.to_string()); let highlights = language .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) diff --git a/crates/editor/src/tasks.rs b/crates/editor/src/tasks.rs index 8be2a3a2e1..0d497e4cac 100644 --- a/crates/editor/src/tasks.rs +++ b/crates/editor/src/tasks.rs @@ -89,7 +89,7 @@ impl Editor { .lsp_task_source()?; if lsp_settings .get(&lsp_tasks_source) - .is_none_or(|s| s.enable_lsp_tasks) + .map_or(true, |s| s.enable_lsp_tasks) { let buffer_id = buffer.read(cx).remote_id(); Some((lsp_tasks_source, buffer_id)) diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index 960fecf59a..0a9d5e9535 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -53,7 +53,7 @@ pub fn marked_display_snapshot( let (unmarked_text, markers) = marked_text_offsets(text); let font = Font { - family: ".ZedMono".into(), + family: "Zed Plex Mono".into(), features: FontFeatures::default(), fallbacks: None, weight: FontWeight::default(), @@ -184,12 +184,12 @@ pub fn editor_content_with_blocks(editor: &Entity, cx: &mut VisualTestCo for (row, block) in blocks { match block { Block::Custom(custom_block) => { - if let BlockPlacement::Near(x) = &custom_block.placement - && snapshot.intersects_fold(x.to_point(&snapshot.buffer_snapshot)) - { - continue; + if let BlockPlacement::Near(x) = &custom_block.placement { + if 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,23 +230,26 @@ pub fn editor_content_with_blocks(editor: &Entity, cx: &mut VisualTestCo lines[row as usize].push_str("§ -----"); } } - Block::ExcerptBoundary { height, .. } => { - for row in row.0..row.0 + height { - 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::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("§ -----"); } diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index 3f78fa2f3e..c59786b1eb 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/crates/editor/src/test/editor_lsp_test_context.rs @@ -300,7 +300,6 @@ 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) -> lsp::Range { let snapshot = self.update_editor(|editor, window, cx| editor.snapshot(window, cx)); let start_point = range.start.to_point(&snapshot.buffer_snapshot); @@ -327,7 +326,6 @@ 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); diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index 8c54c265ed..bdf73da5fb 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -119,7 +119,13 @@ 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(ExcerptRange::new), cx); + multibuffer.push_excerpts( + buffer, + ranges + .into_iter() + .map(|range| ExcerptRange::new(range.clone())), + cx, + ); } multibuffer }); @@ -291,8 +297,9 @@ 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().unwrap().read(cx).fs().as_fake()); + let fs = self.update_editor(|editor, _, cx| { + editor.project.as_ref().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"), @@ -304,16 +311,18 @@ impl EditorTestContext { pub fn clear_index_text(&mut self) { self.cx.run_until_parked(); - let fs = - self.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).fs().as_fake()); + let fs = self.update_editor(|editor, _, cx| { + editor.project.as_ref().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().unwrap().read(cx).fs().as_fake()); + let fs = self.update_editor(|editor, _, cx| { + editor.project.as_ref().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"), @@ -324,8 +333,9 @@ impl EditorTestContext { #[track_caller] pub fn assert_index_text(&mut self, expected: Option<&str>) { - let fs = - self.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).fs().as_fake()); + let fs = self.update_editor(|editor, _, cx| { + editor.project.as_ref().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| { @@ -420,7 +430,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.is_empty() { + if expected_selections.len() > 0 { assert!( is_selected, "excerpt {ix} should be selected. got {:?}", diff --git a/crates/eval/build.rs b/crates/eval/build.rs deleted file mode 100644 index 9ab40da0fb..0000000000 --- a/crates/eval/build.rs +++ /dev/null @@ -1,14 +0,0 @@ -fn main() { - let cargo_toml = - std::fs::read_to_string("../zed/Cargo.toml").expect("Failed to read crates/zed/Cargo.toml"); - let version = cargo_toml - .lines() - .find(|line| line.starts_with("version = ")) - .expect("Version not found in crates/zed/Cargo.toml") - .split('=') - .nth(1) - .expect("Invalid version format") - .trim() - .trim_matches('"'); - println!("cargo:rustc-env=ZED_PKG_VERSION={}", version); -} diff --git a/crates/eval/src/assertions.rs b/crates/eval/src/assertions.rs index 01fac186d3..489e4aa22e 100644 --- a/crates/eval/src/assertions.rs +++ b/crates/eval/src/assertions.rs @@ -54,7 +54,7 @@ impl AssertionsReport { pub fn passed_count(&self) -> usize { self.ran .iter() - .filter(|a| a.result.as_ref().is_ok_and(|result| result.passed)) + .filter(|a| a.result.as_ref().map_or(false, |result| result.passed)) .count() } diff --git a/crates/eval/src/eval.rs b/crates/eval/src/eval.rs index 9e0504abca..8d257a37a7 100644 --- a/crates/eval/src/eval.rs +++ b/crates/eval/src/eval.rs @@ -13,12 +13,12 @@ pub(crate) use tool_metrics::*; use ::fs::RealFs; use clap::Parser; -use client::{Client, ProxySettings, UserStore}; +use client::{Client, CloudUserStore, ProxySettings, UserStore}; use collections::{HashMap, HashSet}; use extension::ExtensionHostProxy; use futures::future; use gpui::http_client::read_proxy_from_env; -use gpui::{App, AppContext, Application, AsyncApp, Entity, UpdateGlobal}; +use gpui::{App, AppContext, Application, AsyncApp, Entity, SemanticVersion, UpdateGlobal}; use gpui_tokio::Tokio; use language::LanguageRegistry; use language_model::{ConfiguredModel, LanguageModel, LanguageModelRegistry, SelectedModel}; @@ -103,7 +103,7 @@ fn main() { let languages: HashSet = args.languages.into_iter().collect(); let http_client = Arc::new(ReqwestClient::new()); - let app = Application::headless().with_http_client(http_client); + let app = Application::headless().with_http_client(http_client.clone()); 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").is_ok_and(|value| value == "1") + let enable_telemetry = env::var("ZED_EVAL_TELEMETRY").map_or(false, |value| value == "1") && telemetry.has_checksum_seed(); if enable_telemetry { println!("Telemetry enabled"); @@ -167,14 +167,15 @@ fn main() { continue; } - if let Some(language) = meta.language_server - && !languages.contains(&language.file_extension) { + if let Some(language) = meta.language_server { + if !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 @@ -328,6 +329,7 @@ pub struct AgentAppState { pub languages: Arc, pub client: Arc, pub user_store: Entity, + pub cloud_user_store: Entity, pub fs: Arc, pub node_runtime: NodeRuntime, @@ -336,8 +338,7 @@ pub struct AgentAppState { } pub fn init(cx: &mut App) -> Arc { - let app_version = AppVersion::load(env!("ZED_PKG_VERSION")); - release_channel::init(app_version, cx); + release_channel::init(SemanticVersion::default(), cx); gpui_tokio::init(cx); let mut settings_store = SettingsStore::new(cx); @@ -349,8 +350,8 @@ pub fn init(cx: &mut App) -> Arc { // Set User-Agent so we can download language servers from GitHub let user_agent = format!( - "Zed Agent Eval/{} ({}; {})", - app_version, + "Zed/{} ({}; {})", + AppVersion::global(cx), std::env::consts::OS, std::env::consts::ARCH ); @@ -383,6 +384,8 @@ pub fn init(cx: &mut App) -> Arc { let languages = Arc::new(languages); let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); + let cloud_user_store = + cx.new(|cx| CloudUserStore::new(client.cloud_client(), user_store.clone(), cx)); extension::init(cx); @@ -416,9 +419,18 @@ pub fn init(cx: &mut App) -> Arc { language::init(cx); debug_adapter_extension::init(extension_host_proxy.clone(), cx); - language_extension::init(LspAccess::Noop, extension_host_proxy, languages.clone()); + language_extension::init( + LspAccess::Noop, + extension_host_proxy.clone(), + languages.clone(), + ); language_model::init(client.clone(), cx); - language_models::init(user_store.clone(), client.clone(), cx); + language_models::init( + user_store.clone(), + cloud_user_store.clone(), + client.clone(), + cx, + ); languages::init(languages.clone(), node_runtime.clone(), cx); prompt_store::init(cx); terminal_view::init(cx); @@ -443,6 +455,7 @@ pub fn init(cx: &mut App) -> Arc { languages, client, user_store, + cloud_user_store, fs, node_runtime, prompt_builder, @@ -515,7 +528,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!( @@ -526,7 +539,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, + thread_evaluation = judge_output.thread.clone(), tool_metrics = run_output.tool_metrics, response_count = run_output.response_count, token_usage = run_output.token_usage, @@ -706,7 +719,7 @@ fn print_report( println!("Average thread score: {average_thread_score}%"); } - println!(); + println!(""); print_h2("CUMULATIVE TOOL METRICS"); println!("{}", cumulative_tool_metrics); diff --git a/crates/eval/src/example.rs b/crates/eval/src/example.rs index 457b62e98c..23c8814916 100644 --- a/crates/eval/src/example.rs +++ b/crates/eval/src/example.rs @@ -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_message_content(), + text: message.to_string(), tool_use: thread .tool_uses_for_message(message.id, cx) .into_iter() diff --git a/crates/eval/src/examples/add_arg_to_trait_method.rs b/crates/eval/src/examples/add_arg_to_trait_method.rs index 084f12bc62..9c538f9260 100644 --- a/crates/eval/src/examples/add_arg_to_trait_method.rs +++ b/crates/eval/src/examples/add_arg_to_trait_method.rs @@ -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.is_some_and(|edits| { + let ignored = edits.map_or(false, |edits| { edits.has_added_line(" _window: Option,\n") }); - let uningored = edits.is_some_and(|edits| { + let uningored = edits.map_or(false, |edits| { edits.has_added_line(" window: Option,\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.is_some_and(|edits| { + batch_tool_edits.map_or(false, |edits| { edits.has_added_line(" window: Option,\n") }), "Argument: batch_tool", diff --git a/crates/eval/src/explorer.rs b/crates/eval/src/explorer.rs index 3326070cea..ee1dfa95c3 100644 --- a/crates/eval/src/explorer.rs +++ b/crates/eval/src/explorer.rs @@ -46,25 +46,27 @@ fn find_target_files_recursive( max_depth, found_files, )?; - } 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); + } 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); + } + } + } } } Ok(()) } pub fn generate_explorer_html(input_paths: &[PathBuf], output_path: &PathBuf) -> Result { - if let Some(parent) = output_path.parent() - && !parent.exists() - { - fs::create_dir_all(parent).context(format!( - "Failed to create output directory: {}", - parent.display() - ))?; + if let Some(parent) = output_path.parent() { + if !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"); diff --git a/crates/eval/src/instance.rs b/crates/eval/src/instance.rs index c6e4e0b6ec..54d864ea21 100644 --- a/crates/eval/src/instance.rs +++ b/crates/eval/src/instance.rs @@ -90,8 +90,11 @@ impl ExampleInstance { worktrees_dir: &Path, repetition: usize, ) -> Self { - let name = thread.meta().name; - let run_directory = run_dir.join(&name).join(repetition.to_string()); + let name = thread.meta().name.to_string(); + let run_directory = run_dir + .join(&name) + .join(repetition.to_string()) + .to_path_buf(); let repo_path = repo_path_for_url(repos_dir, &thread.meta().url); @@ -218,6 +221,7 @@ impl ExampleInstance { let prompt_store = None; let thread_store = ThreadStore::load( project.clone(), + app_state.cloud_user_store.clone(), tools, prompt_store, app_state.prompt_builder.clone(), @@ -373,10 +377,11 @@ impl ExampleInstance { ); let result = this.thread.conversation(&mut example_cx).await; - if let Err(err) = result - && !err.is::() { + if let Err(err) = result { + if !err.is::() { return Err(err); } + } println!("{}Stopped", this.log_prefix); @@ -455,8 +460,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); @@ -657,7 +662,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() }) @@ -675,8 +680,8 @@ pub fn wait_for_lang_server( [ cx.subscribe(&lsp_store, { let log_prefix = log_prefix.clone(); - move |_, event, _| { - if let project::LspStoreEvent::LanguageServerUpdate { + move |_, event, _| match event { + project::LspStoreEvent::LanguageServerUpdate { message: client::proto::update_language_server::Variant::WorkProgress( LspWorkProgress { @@ -685,13 +690,11 @@ pub fn wait_for_lang_server( }, ), .. - } = event - { - println!("{}⟲ {message}", log_prefix) - } + } => println!("{}⟲ {message}", log_prefix), + _ => {} } }), - cx.subscribe(project, { + cx.subscribe(&project, { let buffer = buffer.clone(); move |project, event, cx| match event { project::Event::LanguageServerAdded(_, _, _) => { @@ -769,7 +772,7 @@ pub async fn query_lsp_diagnostics( } fn parse_assertion_result(response: &str) -> Result { - let analysis = get_tag("analysis", response)?; + let analysis = get_tag("analysis", response)?.to_string(); let passed = match get_tag("passed", response)?.to_lowercase().as_str() { "true" => true, "false" => false, @@ -836,7 +839,7 @@ fn messages_to_markdown<'a>(message_iter: impl IntoIterator) 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 } => { @@ -844,7 +847,7 @@ fn messages_to_markdown<'a>(message_iter: impl IntoIterator) 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) => { @@ -876,7 +879,7 @@ pub async fn send_language_model_request( request: LanguageModelRequest, cx: &AsyncApp, ) -> anyhow::Result { - 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 { @@ -913,9 +916,9 @@ impl RequestMarkdown { for tool in &request.tools { write!(&mut tools, "# {}\n\n", tool.name).unwrap(); write!(&mut tools, "{}\n\n", tool.description).unwrap(); - writeln!( + write!( &mut tools, - "{}", + "{}\n", MarkdownCodeBlock { tag: "json", text: &format!("{:#}", tool.input_schema) @@ -1189,7 +1192,7 @@ mod test { output.analysis, Some("The model did a good job but there were still compilations errors.".into()) ); - assert!(output.passed); + assert_eq!(output.passed, true); let response = r#" Text around ignored @@ -1209,6 +1212,6 @@ mod test { output.analysis, Some("Failed to compile:\n- Error 1\n- Error 2".into()) ); - assert!(!output.passed); + assert_eq!(output.passed, false); } } diff --git a/crates/extension/src/extension.rs b/crates/extension/src/extension.rs index 6af793253b..35f7f41938 100644 --- a/crates/extension/src/extension.rs +++ b/crates/extension/src/extension.rs @@ -178,15 +178,16 @@ 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" { - 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() - ); + 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() + ); + } } } } diff --git a/crates/extension/src/extension_builder.rs b/crates/extension/src/extension_builder.rs index 3a3026f19c..621ba9250c 100644 --- a/crates/extension/src/extension_builder.rs +++ b/crates/extension/src/extension_builder.rs @@ -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).is_ok_and(|metadata| metadata.is_file()) { + if fs::metadata(&clang_path).map_or(false, |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,10 +484,14 @@ impl ExtensionBuilder { _ => {} } - if let CustomSection(c) = &payload - && strip_custom_section(c.name()) - { - continue; + match &payload { + CustomSection(c) => { + if strip_custom_section(c.name()) { + continue; + } + } + + _ => {} } if let Some((id, range)) = payload.as_section() { RawSection { diff --git a/crates/extension/src/extension_events.rs b/crates/extension/src/extension_events.rs index 94f3277b05..b151b3f412 100644 --- a/crates/extension/src/extension_events.rs +++ b/crates/extension/src/extension_events.rs @@ -19,8 +19,9 @@ pub struct ExtensionEvents; impl ExtensionEvents { /// Returns the global [`ExtensionEvents`]. pub fn try_global(cx: &App) -> Option> { - cx.try_global::() - .map(|g| g.0.clone()) + return cx + .try_global::() + .map(|g| g.0.clone()); } fn new(_cx: &mut Context) -> Self { diff --git a/crates/extension/src/extension_host_proxy.rs b/crates/extension/src/extension_host_proxy.rs index 6a24e3ba3f..917739759f 100644 --- a/crates/extension/src/extension_host_proxy.rs +++ b/crates/extension/src/extension_host_proxy.rs @@ -28,6 +28,7 @@ pub struct ExtensionHostProxy { snippet_proxy: RwLock>>, slash_command_proxy: RwLock>>, context_server_proxy: RwLock>>, + indexed_docs_provider_proxy: RwLock>>, debug_adapter_provider_proxy: RwLock>>, } @@ -53,6 +54,7 @@ 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(), } } @@ -85,6 +87,14 @@ 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() @@ -398,6 +408,30 @@ impl ExtensionContextServerProxy for ExtensionHostProxy { } } +pub trait ExtensionIndexedDocsProviderProxy: Send + Sync + 'static { + fn register_indexed_docs_provider(&self, extension: Arc, provider_id: Arc); + + fn unregister_indexed_docs_provider(&self, provider_id: Arc); +} + +impl ExtensionIndexedDocsProviderProxy for ExtensionHostProxy { + fn register_indexed_docs_provider(&self, extension: Arc, provider_id: Arc) { + 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) { + 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, diff --git a/crates/extension/src/extension_manifest.rs b/crates/extension/src/extension_manifest.rs index f5296198b0..e3235cf561 100644 --- a/crates/extension/src/extension_manifest.rs +++ b/crates/extension/src/extension_manifest.rs @@ -84,6 +84,8 @@ pub struct ExtensionManifest { #[serde(default)] pub slash_commands: BTreeMap, SlashCommandManifestEntry>, #[serde(default)] + pub indexed_docs_providers: BTreeMap, IndexedDocsProviderEntry>, + #[serde(default)] pub snippets: Option, #[serde(default)] pub capabilities: Vec, @@ -161,7 +163,7 @@ pub struct LanguageServerManifestEntry { #[serde(default)] languages: Vec, #[serde(default)] - pub language_ids: HashMap, + pub language_ids: HashMap, #[serde(default)] pub code_action_kinds: Option>, } @@ -193,6 +195,9 @@ 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, @@ -266,6 +271,7 @@ 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(), @@ -298,6 +304,7 @@ 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(), diff --git a/crates/extension_api/src/extension_api.rs b/crates/extension_api/src/extension_api.rs index 72327179ee..aacc5d8795 100644 --- a/crates/extension_api/src/extension_api.rs +++ b/crates/extension_api/src/extension_api.rs @@ -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. diff --git a/crates/extension_cli/src/main.rs b/crates/extension_cli/src/main.rs index d6c0501efd..ab4a9cddb0 100644 --- a/crates/extension_cli/src/main.rs +++ b/crates/extension_cli/src/main.rs @@ -144,6 +144,10 @@ fn extension_provides(manifest: &ExtensionManifest) -> BTreeSet ExtensionManifest { .collect(), context_servers: BTreeMap::default(), slash_commands: BTreeMap::default(), + indexed_docs_providers: BTreeMap::default(), snippets: None, capabilities: vec![ExtensionCapability::ProcessExec( extension::ProcessExecCapability { diff --git a/crates/extension_host/src/capability_granter.rs b/crates/extension_host/src/capability_granter.rs index 5491967e08..c77e5ecba1 100644 --- a/crates/extension_host/src/capability_granter.rs +++ b/crates/extension_host/src/capability_granter.rs @@ -108,6 +108,7 @@ 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(), @@ -145,7 +146,7 @@ mod tests { command: "*".to_string(), args: vec!["**".to_string()], })], - manifest, + manifest.clone(), ); assert!(granter.grant_exec("ls", &["-la"]).is_ok()); } diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index fde0aeac94..dc38c244f1 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -16,9 +16,9 @@ pub use extension::ExtensionManifest; use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder}; use extension::{ ExtensionContextServerProxy, ExtensionDebugAdapterProviderProxy, ExtensionEvents, - ExtensionGrammarProxy, ExtensionHostProxy, ExtensionLanguageProxy, - ExtensionLanguageServerProxy, ExtensionSlashCommandProxy, ExtensionSnippetProxy, - ExtensionThemeProxy, + ExtensionGrammarProxy, ExtensionHostProxy, ExtensionIndexedDocsProviderProxy, + ExtensionLanguageProxy, ExtensionLanguageServerProxy, ExtensionSlashCommandProxy, + ExtensionSnippetProxy, ExtensionThemeProxy, }; use fs::{Fs, RemoveOptions}; use futures::future::join_all; @@ -93,9 +93,10 @@ 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) { - return false; + if !is_supported_wasm_api_version(release_channel, wasm_api_version) { + return false; + } } true @@ -291,17 +292,19 @@ 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 - && 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; + 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; + } + } } } @@ -389,9 +392,10 @@ impl ExtensionStore { if let Some(path::Component::Normal(extension_dir_name)) = event_path.components().next() - && let Some(extension_id) = extension_dir_name.to_str() { - reload_tx.unbounded_send(Some(extension_id.into())).ok(); + if let Some(extension_id) = extension_dir_name.to_str() { + reload_tx.unbounded_send(Some(extension_id.into())).ok(); + } } } } @@ -562,12 +566,12 @@ impl ExtensionStore { extensions .into_iter() .filter(|extension| { - this.extension_index - .extensions - .get(&extension.id) - .is_none_or(|installed_extension| { + this.extension_index.extensions.get(&extension.id).map_or( + true, + |installed_extension| { installed_extension.manifest.version != extension.manifest.version - }) + }, + ) }) .collect() }) @@ -759,8 +763,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) - && let Some(manifest) = this.extension_manifest_for_id(&extension_id) { + 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()), @@ -768,6 +772,7 @@ impl ExtensionStore { ) }); } + } }) .ok(); } @@ -907,12 +912,12 @@ impl ExtensionStore { extension_store.update(cx, |_, cx| { cx.emit(Event::ExtensionUninstalled(extension_id.clone())); - 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) - }); + 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) + }); + } } })?; @@ -992,12 +997,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) - && 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) { + if let Some(manifest) = this.extension_manifest_for_id(&extension_id) { + events.update(cx, |this, cx| { + this.emit(extension::Event::ExtensionInstalled(manifest.clone()), cx) + }); + } } })?; @@ -1113,17 +1118,15 @@ impl ExtensionStore { extensions_to_unload.len() - reload_count ); - let extension_ids = extensions_to_load - .iter() - .filter_map(|id| { - Some(( - id.clone(), - new_index.extensions.get(id)?.manifest.version.clone(), - )) - }) - .collect::>(); - - telemetry::event!("Extensions Loaded", id_and_versions = extension_ids); + for extension_id in &extensions_to_load { + if let Some(extension) = new_index.extensions.get(extension_id) { + telemetry::event!( + "Extension Loaded", + extension_id, + version = extension.manifest.version + ); + } + } let themes_to_remove = old_index .themes @@ -1175,18 +1178,22 @@ impl ExtensionStore { } } - for server_id in extension.manifest.context_servers.keys() { + for (server_id, _) in &extension.manifest.context_servers { self.proxy.unregister_context_server(server_id.clone(), cx); } - for adapter in extension.manifest.debug_adapters.keys() { + for (adapter, _) in &extension.manifest.debug_adapters { self.proxy.unregister_debug_adapter(adapter.clone()); } - for locator in extension.manifest.debug_locators.keys() { + for (locator, _) in &extension.manifest.debug_locators { self.proxy.unregister_debug_locator(locator.clone()); } - for command_name in extension.manifest.slash_commands.keys() { + for (command_name, _) in &extension.manifest.slash_commands { 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 @@ -1270,7 +1277,6 @@ impl ExtensionStore { queries, context_provider, toolchain_provider: None, - manifest_name: None, }) }), ); @@ -1336,7 +1342,7 @@ impl ExtensionStore { &extension_path, &extension.manifest, wasm_host.clone(), - cx, + &cx, ) .await .with_context(|| format!("Loading extension from {extension_path:?}")); @@ -1386,11 +1392,16 @@ impl ExtensionStore { ); } - for id in manifest.context_servers.keys() { + for (id, _context_server_entry) in &manifest.context_servers { 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())); @@ -1451,7 +1462,7 @@ impl ExtensionStore { if extension_dir .file_name() - .is_some_and(|file_name| file_name == ".DS_Store") + .map_or(false, |file_name| file_name == ".DS_Store") { continue; } @@ -1675,8 +1686,9 @@ impl ExtensionStore { let schema_path = &extension::build_debug_adapter_schema_path(adapter_name, meta); if fs.is_file(&src_dir.join(schema_path)).await { - if let Some(parent) = schema_path.parent() { - fs.create_dir(&tmp_dir.join(parent)).await? + match schema_path.parent() { + Some(parent) => fs.create_dir(&tmp_dir.join(parent)).await?, + None => {} } fs.copy_file( &src_dir.join(schema_path), @@ -1770,7 +1782,7 @@ impl ExtensionStore { })?; for client in clients { - Self::sync_extensions_over_ssh(this, client, cx) + Self::sync_extensions_over_ssh(&this, client, cx) .await .log_err(); } @@ -1782,10 +1794,10 @@ impl ExtensionStore { let connection_options = client.read(cx).connection_options(); let ssh_url = connection_options.ssh_url(); - if let Some(existing_client) = self.ssh_clients.get(&ssh_url) - && existing_client.upgrade().is_some() - { - return; + if let Some(existing_client) = self.ssh_clients.get(&ssh_url) { + if existing_client.upgrade().is_some() { + return; + } } self.ssh_clients.insert(ssh_url, client.downgrade()); diff --git a/crates/extension_host/src/extension_store_test.rs b/crates/extension_host/src/extension_store_test.rs index 347a610439..891ab91852 100644 --- a/crates/extension_host/src/extension_store_test.rs +++ b/crates/extension_host/src/extension_store_test.rs @@ -10,7 +10,7 @@ use fs::{FakeFs, Fs, RealFs}; use futures::{AsyncReadExt, StreamExt, io::BufReader}; use gpui::{AppContext as _, SemanticVersion, TestAppContext}; use http_client::{FakeHttpClient, Response}; -use language::{BinaryStatus, LanguageMatcher, LanguageName, LanguageRegistry}; +use language::{BinaryStatus, LanguageMatcher, LanguageRegistry}; use language_extension::LspAccess; use lsp::LanguageServerName; use node_runtime::NodeRuntime; @@ -160,6 +160,7 @@ 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(), @@ -190,6 +191,7 @@ 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(), @@ -304,11 +306,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { assert_eq!( language_registry.language_names(), - [ - LanguageName::new("ERB"), - LanguageName::new("Plain Text"), - LanguageName::new("Ruby"), - ] + ["ERB", "Plain Text", "Ruby"] ); assert_eq!( theme_registry.list_names(), @@ -369,6 +367,7 @@ 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(), @@ -459,11 +458,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { assert_eq!( language_registry.language_names(), - [ - LanguageName::new("ERB"), - LanguageName::new("Plain Text"), - LanguageName::new("Ruby"), - ] + ["ERB", "Plain Text", "Ruby"] ); assert_eq!( language_registry.grammar_names(), @@ -518,10 +513,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { assert_eq!(actual_language.hidden, expected_language.hidden); } - assert_eq!( - language_registry.language_names(), - [LanguageName::new("Plain Text")] - ); + assert_eq!(language_registry.language_names(), ["Plain Text"]); assert_eq!(language_registry.grammar_names(), []); }); } diff --git a/crates/extension_host/src/headless_host.rs b/crates/extension_host/src/headless_host.rs index a6305118cd..adc9638c29 100644 --- a/crates/extension_host/src/headless_host.rs +++ b/crates/extension_host/src/headless_host.rs @@ -163,7 +163,6 @@ impl HeadlessExtensionStore { queries: LanguageQueries::default(), context_provider: None, toolchain_provider: None, - manifest_name: None, }) }), ); @@ -175,7 +174,7 @@ impl HeadlessExtensionStore { } let wasm_extension: Arc = - 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() { diff --git a/crates/extension_host/src/wasm_host.rs b/crates/extension_host/src/wasm_host.rs index c5bc21fc1c..d990b670f4 100644 --- a/crates/extension_host/src/wasm_host.rs +++ b/crates/extension_host/src/wasm_host.rs @@ -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 (timer.next().await).is_some() { + while let Some(_) = timer.next().await { // Exit the loop and thread once the engine is dropped. let Some(engine) = engine_ref.upgrade() else { break; @@ -701,15 +701,16 @@ 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" { - 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() - ); + 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() + ); + } } } } diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs index 84794d5386..767b9033ad 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs @@ -938,7 +938,7 @@ impl ExtensionImports for WasmState { binary: settings.binary.map(|binary| settings::CommandSettings { path: binary.path, arguments: binary.arguments, - env: binary.env.map(|env| env.into_iter().collect()), + env: binary.env, }), settings: settings.settings, initialization_options: settings.initialization_options, diff --git a/crates/extensions_ui/src/components/feature_upsell.rs b/crates/extensions_ui/src/components/feature_upsell.rs index 0515dd46d3..e2e65f1598 100644 --- a/crates/extensions_ui/src/components/feature_upsell.rs +++ b/crates/extensions_ui/src/components/feature_upsell.rs @@ -58,9 +58,10 @@ impl RenderOnce for FeatureUpsell { el.child( Button::new("open_docs", "View Documentation") .icon(IconName::ArrowUpRight) - .icon_size(IconSize::Small) + .icon_size(IconSize::XSmall) .icon_position(IconPosition::End) .on_click({ + let docs_url = docs_url.clone(); move |_event, _window, cx| { telemetry::event!( "Documentation Viewed", diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index fd504764b6..fe3e94f5c2 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -116,7 +116,6 @@ pub fn init(cx: &mut App) { files: false, directories: true, multiple: false, - prompt: None, }, DirectoryLister::Local( workspace.project().clone(), @@ -694,7 +693,7 @@ impl ExtensionsPage { cx.open_url(&repository_url); } })) - .tooltip(Tooltip::text(repository_url)) + .tooltip(Tooltip::text(repository_url.clone())) })), ) } @@ -704,7 +703,7 @@ impl ExtensionsPage { extension: &ExtensionMetadata, cx: &mut Context, ) -> ExtensionCard { - let this = cx.entity(); + let this = cx.entity().clone(); let status = Self::extension_status(&extension.id, cx); let has_dev_extension = Self::dev_extension_exists(&extension.id, cx); @@ -827,7 +826,7 @@ impl ExtensionsPage { cx.open_url(&repository_url); } })) - .tooltip(Tooltip::text(repository_url)), + .tooltip(Tooltip::text(repository_url.clone())), ) .child( PopoverMenu::new(SharedString::from(format!( @@ -863,7 +862,7 @@ impl ExtensionsPage { window: &mut Window, cx: &mut App, ) -> Entity { - ContextMenu::build(window, cx, |context_menu, window, _| { + let context_menu = ContextMenu::build(window, cx, |context_menu, window, _| { context_menu .entry( "Install Another Version...", @@ -887,7 +886,9 @@ impl ExtensionsPage { cx.write_to_clipboard(ClipboardItem::new_string(authors.join(", "))); } }) - }) + }); + + context_menu } fn show_extension_version_list( @@ -1029,14 +1030,15 @@ impl ExtensionsPage { .read(cx) .extension_manifest_for_id(&extension_id) .cloned() - && let Some(events) = extension::ExtensionEvents::try_global(cx) { - events.update(cx, |this, cx| { - this.emit( - extension::Event::ConfigureExtensionRequested(manifest), - cx, - ) - }); + if let Some(events) = extension::ExtensionEvents::try_global(cx) { + events.update(cx, |this, cx| { + this.emit( + extension::Event::ConfigureExtensionRequested(manifest), + cx, + ) + }); + } } } }) diff --git a/crates/feature_flags/src/feature_flags.rs b/crates/feature_flags/src/feature_flags.rs index f5f7fc42b3..631bafc841 100644 --- a/crates/feature_flags/src/feature_flags.rs +++ b/crates/feature_flags/src/feature_flags.rs @@ -14,7 +14,7 @@ struct FeatureFlags { } pub static ZED_DISABLE_STAFF: LazyLock = LazyLock::new(|| { - std::env::var("ZED_DISABLE_STAFF").is_ok_and(|value| !value.is_empty() && value != "0") + std::env::var("ZED_DISABLE_STAFF").map_or(false, |value| !value.is_empty() && value != "0") }); impl FeatureFlags { @@ -77,6 +77,14 @@ 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 { @@ -89,25 +97,10 @@ impl FeatureFlag for JjUiFeatureFlag { const NAME: &'static str = "jj-ui"; } -pub struct GeminiAndNativeFeatureFlag; +pub struct AcpFeatureFlag; -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"; } pub trait FeatureFlagViewExt { @@ -165,11 +158,6 @@ where } } -#[derive(Debug)] -pub struct OnFlagsReady { - pub is_staff: bool, -} - pub trait FeatureFlagAppExt { fn wait_for_flag(&mut self) -> WaitForFlag; @@ -181,10 +169,6 @@ pub trait FeatureFlagAppExt { fn has_flag(&self) -> bool; fn is_staff(&self) -> bool; - fn on_flags_ready(&mut self, callback: F) -> Subscription - where - F: FnMut(OnFlagsReady, &mut App) + 'static; - fn observe_flag(&mut self, callback: F) -> Subscription where F: FnMut(bool, &mut App) + 'static; @@ -205,7 +189,7 @@ impl FeatureFlagAppExt for App { fn has_flag(&self) -> bool { self.try_global::() .map(|flags| flags.has_flag::()) - .unwrap_or(T::enabled_for_all()) + .unwrap_or(false) } fn is_staff(&self) -> bool { @@ -214,21 +198,6 @@ impl FeatureFlagAppExt for App { .unwrap_or(false) } - fn on_flags_ready(&mut self, mut callback: F) -> Subscription - where - F: FnMut(OnFlagsReady, &mut App) + 'static, - { - self.observe_global::(move |cx| { - let feature_flags = cx.global::(); - callback( - OnFlagsReady { - is_staff: feature_flags.staff, - }, - cx, - ); - }) - } - fn observe_flag(&mut self, mut callback: F) -> Subscription where F: FnMut(bool, &mut App) + 'static, diff --git a/crates/feedback/Cargo.toml b/crates/feedback/Cargo.toml index db872f7a15..3a2c1fd713 100644 --- a/crates/feedback/Cargo.toml +++ b/crates/feedback/Cargo.toml @@ -15,9 +15,13 @@ path = "src/feedback.rs" test-support = [] [dependencies] +client.workspace = true gpui.workspace = true +human_bytes = "0.4.1" menu.workspace = true -system_specs.workspace = true +release_channel.workspace = true +serde.workspace = true +sysinfo.workspace = true ui.workspace = true urlencoding.workspace = true util.workspace = true diff --git a/crates/feedback/src/feedback.rs b/crates/feedback/src/feedback.rs index 3822dd7ba3..40c2707d34 100644 --- a/crates/feedback/src/feedback.rs +++ b/crates/feedback/src/feedback.rs @@ -1,14 +1,18 @@ use gpui::{App, ClipboardItem, PromptLevel, actions}; -use system_specs::{CopySystemSpecsIntoClipboard, SystemSpecs}; +use system_specs::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. diff --git a/crates/system_specs/src/system_specs.rs b/crates/feedback/src/system_specs.rs similarity index 58% rename from crates/system_specs/src/system_specs.rs rename to crates/feedback/src/system_specs.rs index 731d335232..7c002d90e9 100644 --- a/crates/system_specs/src/system_specs.rs +++ b/crates/feedback/src/system_specs.rs @@ -1,22 +1,11 @@ -//! # system_specs - use client::telemetry; -pub use gpui::GpuSpecs; -use gpui::{App, AppContext as _, SemanticVersion, Task, Window, actions}; +use gpui::{App, AppContext as _, SemanticVersion, Task, Window}; use human_bytes::human_bytes; use release_channel::{AppCommitSha, AppVersion, ReleaseChannel}; use serde::Serialize; use std::{env, fmt::Display}; use sysinfo::{MemoryRefreshKind, RefreshKind, System}; -actions!( - zed, - [ - /// Copies system specifications to the clipboard for bug reports. - CopySystemSpecsIntoClipboard, - ] -); - #[derive(Clone, Debug, Serialize)] pub struct SystemSpecs { app_version: String, @@ -42,7 +31,7 @@ impl SystemSpecs { let architecture = env::consts::ARCH; let commit_sha = match release_channel { ReleaseChannel::Dev | ReleaseChannel::Nightly => { - AppCommitSha::try_global(cx).map(|sha| sha.full()) + AppCommitSha::try_global(cx).map(|sha| sha.full().clone()) } _ => None, }; @@ -146,7 +135,7 @@ impl Display for SystemSpecs { fn try_determine_available_gpus() -> Option { #[cfg(any(target_os = "linux", target_os = "freebsd"))] { - std::process::Command::new("vulkaninfo") + return std::process::Command::new("vulkaninfo") .args(&["--summary"]) .output() .ok() @@ -161,123 +150,14 @@ fn try_determine_available_gpus() -> Option { ] .join("\n") }) - .or(Some("Failed to run `vulkaninfo --summary`".to_string())) + .or(Some("Failed to run `vulkaninfo --summary`".to_string())); } #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] { - None + return None; } } -#[derive(Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize, Clone)] -pub struct GpuInfo { - pub device_name: Option, - pub device_pci_id: u16, - pub vendor_name: Option, - pub vendor_pci_id: u16, - pub driver_version: Option, - pub driver_name: Option, -} - -#[cfg(any(target_os = "linux", target_os = "freebsd"))] -pub fn read_gpu_info_from_sys_class_drm() -> anyhow::Result> { - use anyhow::Context as _; - use pciid_parser; - let dir_iter = std::fs::read_dir("/sys/class/drm").context("Failed to read /sys/class/drm")?; - let mut pci_addresses = vec![]; - let mut gpus = Vec::::new(); - let pci_db = pciid_parser::Database::read().ok(); - for entry in dir_iter { - let Ok(entry) = entry else { - continue; - }; - - let device_path = entry.path().join("device"); - let Some(pci_address) = device_path.read_link().ok().and_then(|pci_address| { - pci_address - .file_name() - .and_then(std::ffi::OsStr::to_str) - .map(str::trim) - .map(str::to_string) - }) else { - continue; - }; - let Ok(device_pci_id) = read_pci_id_from_path(device_path.join("device")) else { - continue; - }; - let Ok(vendor_pci_id) = read_pci_id_from_path(device_path.join("vendor")) else { - continue; - }; - let driver_name = std::fs::read_link(device_path.join("driver")) - .ok() - .and_then(|driver_link| { - driver_link - .file_name() - .and_then(std::ffi::OsStr::to_str) - .map(str::trim) - .map(str::to_string) - }); - let driver_version = driver_name - .as_ref() - .and_then(|driver_name| { - std::fs::read_to_string(format!("/sys/module/{driver_name}/version")).ok() - }) - .as_deref() - .map(str::trim) - .map(str::to_string); - - let already_found = gpus - .iter() - .zip(&pci_addresses) - .any(|(gpu, gpu_pci_address)| { - gpu_pci_address == &pci_address - && gpu.driver_version == driver_version - && gpu.driver_name == driver_name - }); - - if already_found { - continue; - } - - let vendor = pci_db - .as_ref() - .and_then(|db| db.vendors.get(&vendor_pci_id)); - let vendor_name = vendor.map(|vendor| vendor.name.clone()); - let device_name = vendor - .and_then(|vendor| vendor.devices.get(&device_pci_id)) - .map(|device| device.name.clone()); - - gpus.push(GpuInfo { - device_name, - device_pci_id, - vendor_name, - vendor_pci_id, - driver_version, - driver_name, - }); - pci_addresses.push(pci_address); - } - - Ok(gpus) -} - -#[cfg(any(target_os = "linux", target_os = "freebsd"))] -fn read_pci_id_from_path(path: impl AsRef) -> anyhow::Result { - use anyhow::Context as _; - let id = std::fs::read_to_string(path)?; - let id = id - .trim() - .strip_prefix("0x") - .context("Not a device ID") - .context(id.clone())?; - anyhow::ensure!( - id.len() == 4, - "Not a device id, expected 4 digits, found {}", - id.len() - ); - u16::from_str_radix(id, 16).context("Failed to parse device ID") -} - /// Returns value of `ZED_BUNDLE_TYPE` set at compiletime or else at runtime. /// /// The compiletime value is used by flatpak since it doesn't seem to have a way to provide a diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 7512152324..e5ac70bb58 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -17,7 +17,7 @@ use fuzzy::{CharBag, PathMatch, PathMatchCandidate}; use gpui::{ Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext, Modifiers, ModifiersChangedEvent, ParentElement, Render, Styled, Task, WeakEntity, - Window, actions, rems, + Window, actions, }; use open_path_prompt::OpenPathPrompt; use picker::{Picker, PickerDelegate}; @@ -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 - && (!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 { + if !event.modified() || !init_modifiers.is_subset_of(&event) { + self.init_modifiers = None; + window.dispatch_action(menu::Confirm.boxed_clone(), cx); + } } } @@ -267,9 +267,10 @@ impl FileFinder { ) { self.picker.update(cx, |picker, cx| { picker.delegate.include_ignored = match picker.delegate.include_ignored { - Some(true) => FileFinderSettings::get_global(cx) - .include_ignored - .map(|_| false), + Some(true) => match FileFinderSettings::get_global(cx).include_ignored { + Some(_) => Some(false), + None => None, + }, Some(false) => Some(true), None => Some(true), }; @@ -322,34 +323,34 @@ impl FileFinder { ) { self.picker.update(cx, |picker, cx| { let delegate = &mut picker.delegate; - 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), + 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), + } } - } - 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); + } } }) } pub fn modal_max_width(width_setting: Option, window: &mut Window) -> Pixels { let window_width = window.viewport_size().width; - let small_width = rems(34.).to_pixels(window.rem_size()); + let small_width = Pixels(545.); match width_setting { None | Some(FileFinderWidth::Small) => small_width, @@ -496,7 +497,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, } } @@ -536,7 +537,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() }) } } @@ -674,17 +675,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) - && 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; + 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; } - prev_position = *p; + return true; } - return true; } } @@ -877,7 +878,9 @@ impl FileFinderDelegate { PathMatchCandidateSet { snapshot: worktree.snapshot(), include_ignored: self.include_ignored.unwrap_or_else(|| { - worktree.root_entry().is_some_and(|entry| entry.is_ignored) + worktree + .root_entry() + .map_or(false, |entry| entry.is_ignored) }), include_root_name, candidates: project::Candidates::Files, @@ -1042,10 +1045,10 @@ impl FileFinderDelegate { ) } else { let mut path = Arc::clone(project_relative_path); - if project_relative_path.as_ref() == Path::new("") - && let Some(absolute_path) = &entry_path.absolute - { - path = Arc::from(absolute_path.as_path()); + if project_relative_path.as_ref() == Path::new("") { + if let Some(absolute_path) = &entry_path.absolute { + path = Arc::from(absolute_path.as_path()); + } } let mut path_match = PathMatch { @@ -1075,21 +1078,23 @@ impl FileFinderDelegate { ), }; - 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 + 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 + } + }) } - }) + } } } @@ -1237,13 +1242,14 @@ impl FileFinderDelegate { /// Skips first history match (that is displayed topmost) if it's currently opened. fn calculate_selected_index(&self, cx: &mut Context>) -> usize { - 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; + 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; + } + } } } @@ -1304,10 +1310,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 - && first_non_history_index > 0 - { - return vec![first_non_history_index - 1]; + if let Some(first_non_history_index) = first_non_history_index { + if first_non_history_index > 0 { + return vec![first_non_history_index - 1]; + } } } Vec::new() @@ -1396,21 +1402,18 @@ 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(); + let raw_query = raw_query.trim().to_owned(); - 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 { + let file_query_end = if path_position.path.to_str().unwrap_or(&raw_query) == raw_query { None } else { // Safe to unwrap as we won't get here when the unwrap in if fails - Some(path.unwrap().len()) + Some(path_position.path.to_str().unwrap().len()) }; let query = FileSearchQuery { @@ -1433,101 +1436,69 @@ impl PickerDelegate for FileFinderDelegate { window: &mut Window, cx: &mut Context>, ) { - 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| { - 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, - ) + 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| { + 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 &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( - 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( + 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, @@ -1535,52 +1506,88 @@ 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(); + 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::() - { - 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()?; + 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::() { + 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(); + } } } @@ -1752,7 +1759,7 @@ impl PickerDelegate for FileFinderDelegate { Some(ContextMenu::build(window, cx, { let focus_handle = focus_handle.clone(); move |menu, _, _| { - menu.context(focus_handle) + menu.context(focus_handle.clone()) .action( "Split Left", pane::SplitLeft.boxed_clone(), diff --git a/crates/file_finder/src/file_finder_tests.rs b/crates/file_finder/src/file_finder_tests.rs index cd0f203d6a..db259ccef8 100644 --- a/crates/file_finder/src/file_finder_tests.rs +++ b/crates/file_finder/src/file_finder_tests.rs @@ -218,7 +218,6 @@ async fn test_matching_paths(cx: &mut TestAppContext) { " ndan ", " band ", "a bandana", - "bandana:", ] { picker .update_in(cx, |picker, window, cx| { @@ -253,53 +252,6 @@ 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); @@ -1662,7 +1614,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"); }); } @@ -2671,7 +2623,7 @@ async fn open_queried_buffer( workspace: &Entity, cx: &mut gpui::VisualTestContext, ) -> Vec { - 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, _| { diff --git a/crates/file_finder/src/open_path_prompt.rs b/crates/file_finder/src/open_path_prompt.rs index 4625872e46..68ba7a78b5 100644 --- a/crates/file_finder/src/open_path_prompt.rs +++ b/crates/file_finder/src/open_path_prompt.rs @@ -75,16 +75,16 @@ impl OpenPathDelegate { .. } => { let mut i = selected_match_index; - 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; + 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; + } } } let id = self.string_matches.get(i)?.candidate_id; @@ -112,7 +112,7 @@ impl OpenPathDelegate { entries, .. } => user_input - .iter() + .into_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| { @@ -637,7 +637,7 @@ impl PickerDelegate for OpenPathDelegate { 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)) }); @@ -653,7 +653,7 @@ impl PickerDelegate for OpenPathDelegate { if parent_path == &self.prompt_root { format!("{}{}", self.prompt_root, candidate.path.string) } else { - candidate.path.string + candidate.path.string.clone() }, match_positions, )), @@ -684,7 +684,7 @@ impl PickerDelegate for OpenPathDelegate { }; StyledText::new(label) .with_default_highlights( - &window.text_style(), + &window.text_style().clone(), vec![( delta..delta + label_len, HighlightStyle::color(Color::Conflict.color(cx)), @@ -694,7 +694,7 @@ impl PickerDelegate for OpenPathDelegate { } else { StyledText::new(format!("{label} (create)")) .with_default_highlights( - &window.text_style(), + &window.text_style().clone(), vec![( delta..delta + label_len, HighlightStyle::color(Color::Created.color(cx)), @@ -728,7 +728,7 @@ impl PickerDelegate for OpenPathDelegate { .child(LabelLike::new().child(label_with_highlights)), ) } - DirectoryState::None { .. } => None, + DirectoryState::None { .. } => return None, } } diff --git a/crates/file_icons/src/file_icons.rs b/crates/file_icons/src/file_icons.rs index 42c00fb12d..2f159771b1 100644 --- a/crates/file_icons/src/file_icons.rs +++ b/crates/file_icons/src/file_icons.rs @@ -33,23 +33,13 @@ impl FileIcons { // TODO: Associate a type with the languages and have the file's language // override these associations - if let Some(mut typ) = path.file_name().and_then(|typ| typ.to_str()) { - // check if file name is in suffixes - // e.g. catch file named `eslint.config.js` instead of `.eslint.config.js` + // check if file name is in suffixes + // e.g. catch file named `eslint.config.js` instead of `.eslint.config.js` + if let Some(typ) = path.file_name().and_then(|typ| typ.to_str()) { let maybe_path = get_icon_from_suffix(typ); if maybe_path.is_some() { return maybe_path; } - - // check if suffix based on first dot is in suffixes - // e.g. consider `module.js` as suffix to angular's module file named `auth.module.js` - while let Some((_, suffix)) = typ.split_once('.') { - let maybe_path = get_icon_from_suffix(suffix); - if maybe_path.is_some() { - return maybe_path; - } - typ = suffix; - } } // primary case: check if the files extension or the hidden file name @@ -72,7 +62,7 @@ impl FileIcons { return maybe_path; } } - this.get_icon_for_type("default", cx) + return this.get_icon_for_type("default", cx); } fn default_icon_theme(cx: &App) -> Option> { diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index 1d4161134e..633fc1fc99 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -51,7 +51,6 @@ ashpd.workspace = true [dev-dependencies] gpui = { workspace = true, features = ["test-support"] } -git = { workspace = true, features = ["test-support"] } [features] test-support = ["gpui/test-support", "git/test-support"] diff --git a/crates/fs/src/fake_git_repo.rs b/crates/fs/src/fake_git_repo.rs index 8a67eddcd7..378a8fb7df 100644 --- a/crates/fs/src/fake_git_repo.rs +++ b/crates/fs/src/fake_git_repo.rs @@ -1,9 +1,8 @@ -use crate::{FakeFs, FakeFsEntry, Fs}; +use crate::{FakeFs, Fs}; use anyhow::{Context as _, Result}; use collections::{HashMap, HashSet}; use futures::future::{self, BoxFuture, join_all}; use git::{ - Oid, blame::Blame, repository::{ AskPassDelegate, Branch, CommitDetails, CommitOptions, FetchOptions, GitRepository, @@ -11,9 +10,8 @@ use git::{ }, status::{FileStatus, GitStatus, StatusCode, TrackedStatus, UnmergedStatus}, }; -use gpui::{AsyncApp, BackgroundExecutor, SharedString, Task}; +use gpui::{AsyncApp, BackgroundExecutor}; use ignore::gitignore::GitignoreBuilder; -use parking_lot::Mutex; use rope::Rope; use smol::future::FutureExt as _; use std::{path::PathBuf, sync::Arc}; @@ -21,7 +19,6 @@ use std::{path::PathBuf, sync::Arc}; #[derive(Clone)] pub struct FakeGitRepository { pub(crate) fs: Arc, - pub(crate) checkpoints: Arc>>, pub(crate) executor: BackgroundExecutor, pub(crate) dot_git_path: PathBuf, pub(crate) repository_dir_path: PathBuf, @@ -186,7 +183,7 @@ impl GitRepository for FakeGitRepository { async move { None }.boxed() } - fn status(&self, path_prefixes: &[RepoPath]) -> Task> { + fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<'_, Result> { let workdir_path = self.dot_git_path.parent().unwrap(); // Load gitignores @@ -314,10 +311,7 @@ impl GitRepository for FakeGitRepository { entries: entries.into(), }) }); - Task::ready(match result { - Ok(result) => result, - Err(e) => Err(e), - }) + async move { result? }.boxed() } fn branches(&self) -> BoxFuture<'_, Result>> { @@ -345,7 +339,7 @@ impl GitRepository for FakeGitRepository { fn create_branch(&self, name: String) -> BoxFuture<'_, Result<()>> { self.with_state_async(true, move |state| { - state.branches.insert(name); + state.branches.insert(name.to_owned()); Ok(()) }) } @@ -408,11 +402,11 @@ impl GitRepository for FakeGitRepository { &self, _paths: Vec, _env: Arc>, - ) -> BoxFuture<'_, Result<()>> { + ) -> BoxFuture> { unimplemented!() } - fn stash_pop(&self, _env: Arc>) -> BoxFuture<'_, Result<()>> { + fn stash_pop(&self, _env: Arc>) -> BoxFuture> { unimplemented!() } @@ -472,57 +466,22 @@ impl GitRepository for FakeGitRepository { } fn checkpoint(&self) -> BoxFuture<'static, Result> { - let executor = self.executor.clone(); - let fs = self.fs.clone(); - let checkpoints = self.checkpoints.clone(); - let repository_dir_path = self.repository_dir_path.parent().unwrap().to_path_buf(); - async move { - executor.simulate_random_delay().await; - let oid = Oid::random(&mut executor.rng()); - let entry = fs.entry(&repository_dir_path)?; - checkpoints.lock().insert(oid, entry); - Ok(GitRepositoryCheckpoint { commit_sha: oid }) - } - .boxed() + unimplemented!() } - fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<'_, Result<()>> { - let executor = self.executor.clone(); - let fs = self.fs.clone(); - let checkpoints = self.checkpoints.clone(); - let repository_dir_path = self.repository_dir_path.parent().unwrap().to_path_buf(); - async move { - executor.simulate_random_delay().await; - let checkpoints = checkpoints.lock(); - let entry = checkpoints - .get(&checkpoint.commit_sha) - .context(format!("invalid checkpoint: {}", checkpoint.commit_sha))?; - fs.insert_entry(&repository_dir_path, entry.clone())?; - Ok(()) - } - .boxed() + fn restore_checkpoint( + &self, + _checkpoint: GitRepositoryCheckpoint, + ) -> BoxFuture<'_, Result<()>> { + unimplemented!() } fn compare_checkpoints( &self, - left: GitRepositoryCheckpoint, - right: GitRepositoryCheckpoint, + _left: GitRepositoryCheckpoint, + _right: GitRepositoryCheckpoint, ) -> BoxFuture<'_, Result> { - let executor = self.executor.clone(); - let checkpoints = self.checkpoints.clone(); - async move { - executor.simulate_random_delay().await; - let checkpoints = checkpoints.lock(); - let left = checkpoints - .get(&left.commit_sha) - .context(format!("invalid left checkpoint: {}", left.commit_sha))?; - let right = checkpoints - .get(&right.commit_sha) - .context(format!("invalid right checkpoint: {}", right.commit_sha))?; - - Ok(left == right) - } - .boxed() + unimplemented!() } fn diff_checkpoints( @@ -532,68 +491,4 @@ impl GitRepository for FakeGitRepository { ) -> BoxFuture<'_, Result> { unimplemented!() } - - fn default_branch(&self) -> BoxFuture<'_, Result>> { - unimplemented!() - } -} - -#[cfg(test)] -mod tests { - use crate::{FakeFs, Fs}; - use gpui::BackgroundExecutor; - use serde_json::json; - use std::path::Path; - use util::path; - - #[gpui::test] - async fn test_checkpoints(executor: BackgroundExecutor) { - let fs = FakeFs::new(executor); - fs.insert_tree( - path!("/"), - json!({ - "bar": { - "baz": "qux" - }, - "foo": { - ".git": {}, - "a": "lorem", - "b": "ipsum", - }, - }), - ) - .await; - fs.with_git_state(Path::new("/foo/.git"), true, |_git| {}) - .unwrap(); - let repository = fs.open_repo(Path::new("/foo/.git")).unwrap(); - - let checkpoint_1 = repository.checkpoint().await.unwrap(); - fs.write(Path::new("/foo/b"), b"IPSUM").await.unwrap(); - fs.write(Path::new("/foo/c"), b"dolor").await.unwrap(); - let checkpoint_2 = repository.checkpoint().await.unwrap(); - let checkpoint_3 = repository.checkpoint().await.unwrap(); - - assert!( - repository - .compare_checkpoints(checkpoint_2.clone(), checkpoint_3.clone()) - .await - .unwrap() - ); - assert!( - !repository - .compare_checkpoints(checkpoint_1.clone(), checkpoint_2.clone()) - .await - .unwrap() - ); - - repository.restore_checkpoint(checkpoint_1).await.unwrap(); - assert_eq!( - fs.files_with_contents(Path::new("")), - [ - (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()) - ] - ); - } } diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 75312c5c0c..a76ccee2bf 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -12,7 +12,7 @@ use gpui::BackgroundExecutor; use gpui::Global; use gpui::ReadGlobal as _; use std::borrow::Cow; -use util::command::{new_smol_command, new_std_command}; +use util::command::new_std_command; #[cfg(unix)] use std::os::fd::{AsFd, AsRawFd}; @@ -134,7 +134,6 @@ pub trait Fs: Send + Sync { fn home_dir(&self) -> Option; fn open_repo(&self, abs_dot_git: &Path) -> Option>; fn git_init(&self, abs_work_directory: &Path, fallback_branch_name: String) -> Result<()>; - async fn git_clone(&self, repo_url: &str, abs_work_directory: &Path) -> Result<()>; fn is_fake(&self) -> bool; async fn is_case_sensitive(&self) -> Result; @@ -420,19 +419,18 @@ 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 - && 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 { + if 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 { @@ -468,11 +466,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 - && 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 { + 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; + } } let file = smol::fs::File::open(path).await?; match trash::trash_file(&file.as_fd()).await { @@ -625,13 +623,13 @@ impl Fs for RealFs { async fn is_file(&self, path: &Path) -> bool { smol::fs::metadata(path) .await - .is_ok_and(|metadata| metadata.is_file()) + .map_or(false, |metadata| metadata.is_file()) } async fn is_dir(&self, path: &Path) -> bool { smol::fs::metadata(path) .await - .is_ok_and(|metadata| metadata.is_dir()) + .map_or(false, |metadata| metadata.is_dir()) } async fn metadata(&self, path: &Path) -> Result> { @@ -767,23 +765,24 @@ impl Fs for RealFs { let pending_paths: Arc>> = Default::default(); let watcher = Arc::new(fs_watcher::FsWatcher::new(tx, pending_paths.clone())); - // 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}"); + 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}"); + } + } } // 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() - && 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() { + 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(); + } } } watcher.add(&target).ok(); @@ -840,23 +839,6 @@ impl Fs for RealFs { Ok(()) } - async fn git_clone(&self, repo_url: &str, abs_work_directory: &Path) -> Result<()> { - let output = new_smol_command("git") - .current_dir(abs_work_directory) - .args(&["clone", repo_url]) - .output() - .await?; - - if !output.status.success() { - anyhow::bail!( - "git clone failed: {}", - String::from_utf8_lossy(&output.stderr) - ); - } - - Ok(()) - } - fn is_fake(&self) -> bool { false } @@ -924,7 +906,7 @@ pub struct FakeFs { #[cfg(any(test, feature = "test-support"))] struct FakeFsState { - root: FakeFsEntry, + root: Arc>, next_inode: u64, next_mtime: SystemTime, git_event_tx: smol::channel::Sender, @@ -939,7 +921,7 @@ struct FakeFsState { } #[cfg(any(test, feature = "test-support"))] -#[derive(Clone, Debug)] +#[derive(Debug)] enum FakeFsEntry { File { inode: u64, @@ -953,7 +935,7 @@ enum FakeFsEntry { inode: u64, mtime: MTime, len: u64, - entries: BTreeMap, + entries: BTreeMap>>, git_repo_state: Option>>, }, Symlink { @@ -961,67 +943,6 @@ enum FakeFsEntry { }, } -#[cfg(any(test, feature = "test-support"))] -impl PartialEq for FakeFsEntry { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - ( - Self::File { - inode: l_inode, - mtime: l_mtime, - len: l_len, - content: l_content, - git_dir_path: l_git_dir_path, - }, - Self::File { - inode: r_inode, - mtime: r_mtime, - len: r_len, - content: r_content, - git_dir_path: r_git_dir_path, - }, - ) => { - l_inode == r_inode - && l_mtime == r_mtime - && l_len == r_len - && l_content == r_content - && l_git_dir_path == r_git_dir_path - } - ( - Self::Dir { - inode: l_inode, - mtime: l_mtime, - len: l_len, - entries: l_entries, - git_repo_state: l_git_repo_state, - }, - Self::Dir { - inode: r_inode, - mtime: r_mtime, - len: r_len, - entries: r_entries, - git_repo_state: r_git_repo_state, - }, - ) => { - let same_repo_state = match (l_git_repo_state.as_ref(), r_git_repo_state.as_ref()) { - (Some(l), Some(r)) => Arc::ptr_eq(l, r), - (None, None) => true, - _ => false, - }; - l_inode == r_inode - && l_mtime == r_mtime - && l_len == r_len - && l_entries == r_entries - && same_repo_state - } - (Self::Symlink { target: l_target }, Self::Symlink { target: r_target }) => { - l_target == r_target - } - _ => false, - } - } -} - #[cfg(any(test, feature = "test-support"))] impl FakeFsState { fn get_and_increment_mtime(&mut self) -> MTime { @@ -1036,9 +957,25 @@ impl FakeFsState { inode } - fn canonicalize(&self, target: &Path, follow_symlink: bool) -> Option { - let mut canonical_path = PathBuf::new(); + fn read_path(&self, target: &Path) -> Result>> { + Ok(self + .try_read_path(target, true) + .ok_or_else(|| { + anyhow!(io::Error::new( + io::ErrorKind::NotFound, + format!("not found: {target:?}") + )) + })? + .0) + } + + fn try_read_path( + &self, + target: &Path, + follow_symlink: bool, + ) -> Option<(Arc>, PathBuf)> { let mut path = target.to_path_buf(); + let mut canonical_path = PathBuf::new(); let mut entry_stack = Vec::new(); 'outer: loop { let mut path_components = path.components().peekable(); @@ -1048,7 +985,7 @@ impl FakeFsState { Component::Prefix(prefix_component) => prefix = Some(prefix_component), Component::RootDir => { entry_stack.clear(); - entry_stack.push(&self.root); + entry_stack.push(self.root.clone()); canonical_path.clear(); match prefix { Some(prefix_component) => { @@ -1065,18 +1002,20 @@ impl FakeFsState { canonical_path.pop(); } Component::Normal(name) => { - 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) - && let FakeFsEntry::Symlink { target, .. } = entry - { - let mut target = target.clone(); - target.extend(path_components); - path = target; - continue 'outer; + let current_entry = entry_stack.last().cloned()?; + let current_entry = current_entry.lock(); + if let FakeFsEntry::Dir { entries, .. } = &*current_entry { + let entry = entries.get(name.to_str().unwrap()).cloned()?; + if path_components.peek().is_some() || follow_symlink { + let entry = entry.lock(); + if let FakeFsEntry::Symlink { target, .. } = &*entry { + let mut target = target.clone(); + target.extend(path_components); + path = target; + continue 'outer; + } } - entry_stack.push(entry); + entry_stack.push(entry.clone()); canonical_path = canonical_path.join(name); } else { return None; @@ -1086,74 +1025,19 @@ impl FakeFsState { } break; } - - if entry_stack.is_empty() { - None - } else { - Some(canonical_path) - } + Some((entry_stack.pop()?, canonical_path)) } - fn try_entry( - &mut self, - target: &Path, - follow_symlink: bool, - ) -> Option<(&mut FakeFsEntry, PathBuf)> { - let canonical_path = self.canonicalize(target, follow_symlink)?; - - 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 {:?}", - target, canonical_path - ) - }; - - let mut entry = &mut self.root; - for component in components { - match component { - Component::Normal(name) => { - if let FakeFsEntry::Dir { entries, .. } = entry { - entry = entries.get_mut(name.to_str().unwrap())?; - } else { - return None; - } - } - _ => { - panic!( - "the path {:?} was not canonicalized properly {:?}", - target, canonical_path - ) - } - } - } - - Some((entry, canonical_path)) - } - - fn entry(&mut self, target: &Path) -> Result<&mut FakeFsEntry> { - Ok(self - .try_entry(target, true) - .ok_or_else(|| { - anyhow!(io::Error::new( - io::ErrorKind::NotFound, - format!("not found: {target:?}") - )) - })? - .0) - } - - fn write_path(&mut self, path: &Path, callback: Fn) -> Result + fn write_path(&self, path: &Path, callback: Fn) -> Result where - Fn: FnOnce(btree_map::Entry) -> Result, + Fn: FnOnce(btree_map::Entry>>) -> Result, { let path = normalize_path(path); let filename = path.file_name().context("cannot overwrite the root")?; let parent_path = path.parent().unwrap(); - let parent = self.entry(parent_path)?; + let parent = self.read_path(parent_path)?; + let mut parent = parent.lock(); let new_entry = parent .dir_entries(parent_path)? .entry(filename.to_str().unwrap().into()); @@ -1203,13 +1087,13 @@ impl FakeFs { this: this.clone(), executor: executor.clone(), state: Arc::new(Mutex::new(FakeFsState { - root: FakeFsEntry::Dir { + root: Arc::new(Mutex::new(FakeFsEntry::Dir { inode: 0, mtime: MTime(UNIX_EPOCH), len: 0, entries: Default::default(), git_repo_state: None, - }, + })), git_event_tx: tx, next_mtime: UNIX_EPOCH + Self::SYSTEMTIME_INTERVAL, next_inode: 1, @@ -1259,15 +1143,15 @@ impl FakeFs { .write_path(path, move |entry| { match entry { btree_map::Entry::Vacant(e) => { - e.insert(FakeFsEntry::File { + e.insert(Arc::new(Mutex::new(FakeFsEntry::File { inode: new_inode, mtime: new_mtime, content: Vec::new(), len: 0, git_dir_path: None, - }); + }))); } - btree_map::Entry::Occupied(mut e) => match &mut *e.get_mut() { + btree_map::Entry::Occupied(mut e) => match &mut *e.get_mut().lock() { FakeFsEntry::File { mtime, .. } => *mtime = new_mtime, FakeFsEntry::Dir { mtime, .. } => *mtime = new_mtime, FakeFsEntry::Symlink { .. } => {} @@ -1286,7 +1170,7 @@ impl FakeFs { pub async fn insert_symlink(&self, path: impl AsRef, target: PathBuf) { let mut state = self.state.lock(); let path = path.as_ref(); - let file = FakeFsEntry::Symlink { target }; + let file = Arc::new(Mutex::new(FakeFsEntry::Symlink { target })); state .write_path(path.as_ref(), move |e| match e { btree_map::Entry::Vacant(e) => { @@ -1319,13 +1203,13 @@ impl FakeFs { match entry { btree_map::Entry::Vacant(e) => { kind = Some(PathEventKind::Created); - e.insert(FakeFsEntry::File { + e.insert(Arc::new(Mutex::new(FakeFsEntry::File { inode: new_inode, mtime: new_mtime, len: new_len, content: new_content, git_dir_path: None, - }); + }))); } btree_map::Entry::Occupied(mut e) => { kind = Some(PathEventKind::Changed); @@ -1335,7 +1219,7 @@ impl FakeFs { len, content, .. - } = e.get_mut() + } = &mut *e.get_mut().lock() { *mtime = new_mtime; *content = new_content; @@ -1357,8 +1241,9 @@ impl FakeFs { pub fn read_file_sync(&self, path: impl AsRef) -> Result> { let path = path.as_ref(); let path = normalize_path(path); - let mut state = self.state.lock(); - let entry = state.entry(&path)?; + let state = self.state.lock(); + let entry = state.read_path(&path)?; + let entry = entry.lock(); entry.file_content(&path).cloned() } @@ -1366,8 +1251,9 @@ impl FakeFs { let path = path.as_ref(); let path = normalize_path(path); self.simulate_random_delay().await; - let mut state = self.state.lock(); - let entry = state.entry(&path)?; + let state = self.state.lock(); + let entry = state.read_path(&path)?; + let entry = entry.lock(); entry.file_content(&path).cloned() } @@ -1388,25 +1274,6 @@ impl FakeFs { self.state.lock().flush_events(count); } - pub(crate) fn entry(&self, target: &Path) -> Result { - self.state.lock().entry(target).cloned() - } - - pub(crate) fn insert_entry(&self, target: &Path, new_entry: FakeFsEntry) -> Result<()> { - let mut state = self.state.lock(); - state.write_path(target, |entry| { - match entry { - btree_map::Entry::Vacant(vacant_entry) => { - vacant_entry.insert(new_entry); - } - btree_map::Entry::Occupied(mut occupied_entry) => { - occupied_entry.insert(new_entry); - } - } - Ok(()) - }) - } - #[must_use] pub fn insert_tree<'a>( &'a self, @@ -1476,19 +1343,20 @@ impl FakeFs { F: FnOnce(&mut FakeGitRepositoryState, &Path, &Path) -> T, { let mut state = self.state.lock(); - let git_event_tx = state.git_event_tx.clone(); - let entry = state.entry(dot_git).context("open .git")?; + let entry = state.read_path(dot_git).context("open .git")?; + let mut entry = entry.lock(); - if let FakeFsEntry::Dir { git_repo_state, .. } = entry { + if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry { let repo_state = git_repo_state.get_or_insert_with(|| { log::debug!("insert git state for {dot_git:?}"); - Arc::new(Mutex::new(FakeGitRepositoryState::new(git_event_tx))) + Arc::new(Mutex::new(FakeGitRepositoryState::new( + state.git_event_tx.clone(), + ))) }); let mut repo_state = repo_state.lock(); let result = f(&mut repo_state, dot_git, dot_git); - drop(repo_state); if emit_git_event { state.emit_event([(dot_git, None)]); } @@ -1512,20 +1380,21 @@ impl FakeFs { } } .clone(); - let Some((git_dir_entry, canonical_path)) = state.try_entry(&path, true) else { + drop(entry); + let Some((git_dir_entry, canonical_path)) = state.try_read_path(&path, true) else { anyhow::bail!("pointed-to git dir {path:?} not found") }; let FakeFsEntry::Dir { git_repo_state, entries, .. - } = git_dir_entry + } = &mut *git_dir_entry.lock() else { anyhow::bail!("gitfile points to a non-directory") }; let common_dir = if let Some(child) = entries.get("commondir") { Path::new( - std::str::from_utf8(child.file_content("commondir".as_ref())?) + std::str::from_utf8(child.lock().file_content("commondir".as_ref())?) .context("commondir content")?, ) .to_owned() @@ -1533,14 +1402,15 @@ impl FakeFs { canonical_path.clone() }; let repo_state = git_repo_state.get_or_insert_with(|| { - Arc::new(Mutex::new(FakeGitRepositoryState::new(git_event_tx))) + Arc::new(Mutex::new(FakeGitRepositoryState::new( + state.git_event_tx.clone(), + ))) }); let mut repo_state = repo_state.lock(); let result = f(&mut repo_state, &canonical_path, &common_dir); if emit_git_event { - drop(repo_state); state.emit_event([(canonical_path, None)]); } @@ -1568,10 +1438,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() - && state.current_branch_name.is_none() - { - state.current_branch_name = Some(first.to_string()) + if let Some(first) = branches.first() { + if state.current_branch_name.is_none() { + state.current_branch_name = Some(first.to_string()) + } } state .branches @@ -1679,7 +1549,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(); @@ -1767,12 +1637,14 @@ impl FakeFs { pub fn paths(&self, include_dot_git: bool) -> Vec { let mut result = Vec::new(); let mut queue = collections::VecDeque::new(); - let state = &*self.state.lock(); - queue.push_back((PathBuf::from(util::path!("/")), &state.root)); + queue.push_back(( + PathBuf::from(util::path!("/")), + self.state.lock().root.clone(), + )); while let Some((path, entry)) = queue.pop_front() { - if let FakeFsEntry::Dir { entries, .. } = entry { + if let FakeFsEntry::Dir { entries, .. } = &*entry.lock() { for (name, entry) in entries { - queue.push_back((path.join(name), entry)); + queue.push_back((path.join(name), entry.clone())); } } if include_dot_git @@ -1789,12 +1661,14 @@ impl FakeFs { pub fn directories(&self, include_dot_git: bool) -> Vec { let mut result = Vec::new(); let mut queue = collections::VecDeque::new(); - let state = &*self.state.lock(); - queue.push_back((PathBuf::from(util::path!("/")), &state.root)); + queue.push_back(( + PathBuf::from(util::path!("/")), + self.state.lock().root.clone(), + )); while let Some((path, entry)) = queue.pop_front() { - if let FakeFsEntry::Dir { entries, .. } = entry { + if let FakeFsEntry::Dir { entries, .. } = &*entry.lock() { for (name, entry) in entries { - queue.push_back((path.join(name), entry)); + queue.push_back((path.join(name), entry.clone())); } if include_dot_git || !path @@ -1811,14 +1685,17 @@ impl FakeFs { pub fn files(&self) -> Vec { let mut result = Vec::new(); let mut queue = collections::VecDeque::new(); - let state = &*self.state.lock(); - queue.push_back((PathBuf::from(util::path!("/")), &state.root)); + queue.push_back(( + PathBuf::from(util::path!("/")), + self.state.lock().root.clone(), + )); while let Some((path, entry)) = queue.pop_front() { - match entry { + let e = entry.lock(); + match &*e { FakeFsEntry::File { .. } => result.push(path), FakeFsEntry::Dir { entries, .. } => { for (name, entry) in entries { - queue.push_back((path.join(name), entry)); + queue.push_back((path.join(name), entry.clone())); } } FakeFsEntry::Symlink { .. } => {} @@ -1830,10 +1707,13 @@ impl FakeFs { pub fn files_with_contents(&self, prefix: &Path) -> Vec<(PathBuf, Vec)> { let mut result = Vec::new(); let mut queue = collections::VecDeque::new(); - let state = &*self.state.lock(); - queue.push_back((PathBuf::from(util::path!("/")), &state.root)); + queue.push_back(( + PathBuf::from(util::path!("/")), + self.state.lock().root.clone(), + )); while let Some((path, entry)) = queue.pop_front() { - match entry { + let e = entry.lock(); + match &*e { FakeFsEntry::File { content, .. } => { if path.starts_with(prefix) { result.push((path, content.clone())); @@ -1841,7 +1721,7 @@ impl FakeFs { } FakeFsEntry::Dir { entries, .. } => { for (name, entry) in entries { - queue.push_back((path.join(name), entry)); + queue.push_back((path.join(name), entry.clone())); } } FakeFsEntry::Symlink { .. } => {} @@ -1907,7 +1787,10 @@ impl FakeFsEntry { } } - fn dir_entries(&mut self, path: &Path) -> Result<&mut BTreeMap> { + fn dir_entries( + &mut self, + path: &Path, + ) -> Result<&mut BTreeMap>>> { if let Self::Dir { entries, .. } = self { Ok(entries) } else { @@ -1954,13 +1837,13 @@ struct FakeHandle { impl FileHandle for FakeHandle { fn current_path(&self, fs: &Arc) -> Result { let fs = fs.as_fake(); - let mut state = fs.state.lock(); - let Some(target) = state.moves.get(&self.inode).cloned() else { + let state = fs.state.lock(); + let Some(target) = state.moves.get(&self.inode) else { anyhow::bail!("fake fd not moved") }; - if state.try_entry(&target, false).is_some() { - return Ok(target); + if state.try_read_path(&target, false).is_some() { + return Ok(target.clone()); } anyhow::bail!("fake fd target not found") } @@ -1987,13 +1870,13 @@ impl Fs for FakeFs { state.write_path(&cur_path, |entry| { entry.or_insert_with(|| { created_dirs.push((cur_path.clone(), Some(PathEventKind::Created))); - FakeFsEntry::Dir { + Arc::new(Mutex::new(FakeFsEntry::Dir { inode, mtime, len: 0, entries: Default::default(), git_repo_state: None, - } + })) }); Ok(()) })? @@ -2008,13 +1891,13 @@ impl Fs for FakeFs { let mut state = self.state.lock(); let inode = state.get_and_increment_inode(); let mtime = state.get_and_increment_mtime(); - let file = FakeFsEntry::File { + let file = Arc::new(Mutex::new(FakeFsEntry::File { inode, mtime, len: 0, content: Vec::new(), git_dir_path: None, - }; + })); let mut kind = Some(PathEventKind::Created); state.write_path(path, |entry| { match entry { @@ -2038,7 +1921,7 @@ impl Fs for FakeFs { async fn create_symlink(&self, path: &Path, target: PathBuf) -> Result<()> { let mut state = self.state.lock(); - let file = FakeFsEntry::Symlink { target }; + let file = Arc::new(Mutex::new(FakeFsEntry::Symlink { target })); state .write_path(path.as_ref(), move |e| match e { btree_map::Entry::Vacant(e) => { @@ -2101,7 +1984,7 @@ impl Fs for FakeFs { } })?; - let inode = match moved_entry { + let inode = match *moved_entry.lock() { FakeFsEntry::File { inode, .. } => inode, FakeFsEntry::Dir { inode, .. } => inode, _ => 0, @@ -2150,8 +2033,8 @@ impl Fs for FakeFs { let mut state = self.state.lock(); let mtime = state.get_and_increment_mtime(); let inode = state.get_and_increment_inode(); - let source_entry = state.entry(&source)?; - let content = source_entry.file_content(&source)?.clone(); + let source_entry = state.read_path(&source)?; + let content = source_entry.lock().file_content(&source)?.clone(); let mut kind = Some(PathEventKind::Created); state.write_path(&target, |e| match e { btree_map::Entry::Occupied(e) => { @@ -2165,13 +2048,13 @@ impl Fs for FakeFs { } } btree_map::Entry::Vacant(e) => Ok(Some( - e.insert(FakeFsEntry::File { + e.insert(Arc::new(Mutex::new(FakeFsEntry::File { inode, mtime, len: content.len() as u64, content, git_dir_path: None, - }) + }))) .clone(), )), })?; @@ -2187,7 +2070,8 @@ impl Fs for FakeFs { let base_name = path.file_name().context("cannot remove the root")?; let mut state = self.state.lock(); - let parent_entry = state.entry(parent_path)?; + let parent_entry = state.read_path(parent_path)?; + let mut parent_entry = parent_entry.lock(); let entry = parent_entry .dir_entries(parent_path)? .entry(base_name.to_str().unwrap().into()); @@ -2198,14 +2082,15 @@ impl Fs for FakeFs { anyhow::bail!("{path:?} does not exist"); } } - btree_map::Entry::Occupied(mut entry) => { + btree_map::Entry::Occupied(e) => { { - let children = entry.get_mut().dir_entries(&path)?; + let mut entry = e.get().lock(); + let children = entry.dir_entries(&path)?; if !options.recursive && !children.is_empty() { anyhow::bail!("{path:?} is not empty"); } } - entry.remove(); + e.remove(); } } state.emit_event([(path, Some(PathEventKind::Removed))]); @@ -2219,7 +2104,8 @@ impl Fs for FakeFs { let parent_path = path.parent().context("cannot remove the root")?; let base_name = path.file_name().unwrap(); let mut state = self.state.lock(); - let parent_entry = state.entry(parent_path)?; + let parent_entry = state.read_path(parent_path)?; + let mut parent_entry = parent_entry.lock(); let entry = parent_entry .dir_entries(parent_path)? .entry(base_name.to_str().unwrap().into()); @@ -2229,9 +2115,9 @@ impl Fs for FakeFs { anyhow::bail!("{path:?} does not exist"); } } - btree_map::Entry::Occupied(mut entry) => { - entry.get_mut().file_content(&path)?; - entry.remove(); + btree_map::Entry::Occupied(e) => { + e.get().lock().file_content(&path)?; + e.remove(); } } state.emit_event([(path, Some(PathEventKind::Removed))]); @@ -2245,10 +2131,12 @@ impl Fs for FakeFs { async fn open_handle(&self, path: &Path) -> Result> { self.simulate_random_delay().await; - let mut state = self.state.lock(); - let inode = match state.entry(path)? { - FakeFsEntry::File { inode, .. } => *inode, - FakeFsEntry::Dir { inode, .. } => *inode, + let state = self.state.lock(); + let entry = state.read_path(&path)?; + let entry = entry.lock(); + let inode = match *entry { + FakeFsEntry::File { inode, .. } => inode, + FakeFsEntry::Dir { inode, .. } => inode, _ => unreachable!(), }; Ok(Arc::new(FakeHandle { inode })) @@ -2256,7 +2144,7 @@ impl Fs for FakeFs { async fn load(&self, path: &Path) -> Result { let content = self.load_internal(path).await?; - Ok(String::from_utf8(content)?) + Ok(String::from_utf8(content.clone())?) } async fn load_bytes(&self, path: &Path) -> Result> { @@ -2266,9 +2154,6 @@ impl Fs for FakeFs { async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> { self.simulate_random_delay().await; let path = normalize_path(path.as_path()); - if let Some(path) = path.parent() { - self.create_dir(path).await?; - } self.write_file_internal(path, data.into_bytes(), true)?; Ok(()) } @@ -2298,8 +2183,8 @@ impl Fs for FakeFs { let path = normalize_path(path); self.simulate_random_delay().await; let state = self.state.lock(); - let canonical_path = state - .canonicalize(&path, true) + let (_, canonical_path) = state + .try_read_path(&path, true) .with_context(|| format!("path does not exist: {path:?}"))?; Ok(canonical_path) } @@ -2307,9 +2192,9 @@ impl Fs for FakeFs { async fn is_file(&self, path: &Path) -> bool { let path = normalize_path(path); self.simulate_random_delay().await; - let mut state = self.state.lock(); - if let Some((entry, _)) = state.try_entry(&path, true) { - entry.is_file() + let state = self.state.lock(); + if let Some((entry, _)) = state.try_read_path(&path, true) { + entry.lock().is_file() } else { false } @@ -2326,16 +2211,17 @@ impl Fs for FakeFs { let path = normalize_path(path); let mut state = self.state.lock(); state.metadata_call_count += 1; - if let Some((mut entry, _)) = state.try_entry(&path, false) { - let is_symlink = entry.is_symlink(); + if let Some((mut entry, _)) = state.try_read_path(&path, false) { + let is_symlink = entry.lock().is_symlink(); if is_symlink { - if let Some(e) = state.try_entry(&path, true).map(|e| e.0) { + if let Some(e) = state.try_read_path(&path, true).map(|e| e.0) { entry = e; } else { return Ok(None); } } + let entry = entry.lock(); Ok(Some(match &*entry { FakeFsEntry::File { inode, mtime, len, .. @@ -2367,11 +2253,12 @@ impl Fs for FakeFs { async fn read_link(&self, path: &Path) -> Result { self.simulate_random_delay().await; let path = normalize_path(path); - let mut state = self.state.lock(); + let state = self.state.lock(); let (entry, _) = state - .try_entry(&path, false) + .try_read_path(&path, false) .with_context(|| format!("path does not exist: {path:?}"))?; - if let FakeFsEntry::Symlink { target } = entry { + let entry = entry.lock(); + if let FakeFsEntry::Symlink { target } = &*entry { Ok(target.clone()) } else { anyhow::bail!("not a symlink: {path:?}") @@ -2386,7 +2273,8 @@ impl Fs for FakeFs { let path = normalize_path(path); let mut state = self.state.lock(); state.read_dir_call_count += 1; - let entry = state.entry(&path)?; + let entry = state.read_path(&path)?; + let mut entry = entry.lock(); let children = entry.dir_entries(&path)?; let paths = children .keys() @@ -2412,18 +2300,19 @@ impl Fs for FakeFs { tx, original_path: path.to_owned(), fs_state: self.state.clone(), - prefixes: Mutex::new(vec![path]), + prefixes: Mutex::new(vec![path.to_owned()]), }); ( Box::pin(futures::StreamExt::filter(rx, { let watcher = watcher.clone(); move |events| { let result = events.iter().any(|evt_path| { - watcher + let result = watcher .prefixes .lock() .iter() - .any(|prefix| evt_path.path.starts_with(prefix)) + .any(|prefix| evt_path.path.starts_with(prefix)); + result }); let executor = executor.clone(); async move { @@ -2449,7 +2338,6 @@ impl Fs for FakeFs { dot_git_path: abs_dot_git.to_path_buf(), repository_dir_path: repository_dir_path.to_owned(), common_dir_path: common_dir_path.to_owned(), - checkpoints: Arc::default(), }) as _ }, ) @@ -2464,10 +2352,6 @@ impl Fs for FakeFs { smol::block_on(self.create_dir(&abs_work_directory_path.join(".git"))) } - async fn git_clone(&self, _repo_url: &str, _abs_work_directory: &Path) -> Result<()> { - anyhow::bail!("Git clone is not supported in fake Fs") - } - fn is_fake(&self) -> bool { true } diff --git a/crates/fs/src/fs_watcher.rs b/crates/fs/src/fs_watcher.rs index 6ad03ba6df..9fdf2ad0b1 100644 --- a/crates/fs/src/fs_watcher.rs +++ b/crates/fs/src/fs_watcher.rs @@ -1,9 +1,6 @@ use notify::EventKind; use parking_lot::Mutex; -use std::{ - collections::HashMap, - sync::{Arc, OnceLock}, -}; +use std::sync::{Arc, OnceLock}; use util::{ResultExt, paths::SanitizedPath}; use crate::{PathEvent, PathEventKind, Watcher}; @@ -11,7 +8,6 @@ use crate::{PathEvent, PathEventKind, Watcher}; pub struct FsWatcher { tx: smol::channel::Sender<()>, pending_path_events: Arc>>, - registrations: Mutex, WatcherRegistrationId>>, } impl FsWatcher { @@ -22,24 +18,10 @@ impl FsWatcher { Self { tx, pending_path_events, - registrations: Default::default(), } } } -impl Drop for FsWatcher { - fn drop(&mut self) { - let mut registrations = self.registrations.lock(); - let registrations = registrations.drain(); - - let _ = global(|g| { - for (_, registration) in registrations { - g.remove(registration); - } - }); - } -} - impl Watcher for FsWatcher { fn add(&self, path: &std::path::Path) -> anyhow::Result<()> { let root_path = SanitizedPath::from(path); @@ -47,143 +29,75 @@ impl Watcher for FsWatcher { let tx = self.tx.clone(); let pending_paths = self.pending_path_events.clone(); - let path: Arc = path.into(); + use notify::Watcher; - if self.registrations.lock().contains_key(&path) { - return Ok(()); - } - - let registration_id = global({ - let path = path.clone(); + global({ |g| { - g.add( - path, - notify::RecursiveMode::NonRecursive, - move |event: ¬ify::Event| { - let kind = match event.kind { - EventKind::Create(_) => Some(PathEventKind::Created), - EventKind::Modify(_) => Some(PathEventKind::Changed), - EventKind::Remove(_) => Some(PathEventKind::Removed), - _ => None, - }; - let mut path_events = event - .paths - .iter() - .filter_map(|event_path| { - let event_path = SanitizedPath::from(event_path); - event_path.starts_with(&root_path).then(|| PathEvent { - path: event_path.as_path().to_path_buf(), - kind, - }) + g.add(move |event: ¬ify::Event| { + let kind = match event.kind { + EventKind::Create(_) => Some(PathEventKind::Created), + EventKind::Modify(_) => Some(PathEventKind::Changed), + EventKind::Remove(_) => Some(PathEventKind::Removed), + _ => None, + }; + let mut path_events = event + .paths + .iter() + .filter_map(|event_path| { + let event_path = SanitizedPath::from(event_path); + event_path.starts_with(&root_path).then(|| PathEvent { + path: event_path.as_path().to_path_buf(), + kind, }) - .collect::>(); + }) + .collect::>(); - if !path_events.is_empty() { - path_events.sort(); - let mut pending_paths = pending_paths.lock(); - if pending_paths.is_empty() { - tx.try_send(()).ok(); - } - util::extend_sorted( - &mut *pending_paths, - path_events, - usize::MAX, - |a, b| a.path.cmp(&b.path), - ); + if !path_events.is_empty() { + path_events.sort(); + let mut pending_paths = pending_paths.lock(); + if pending_paths.is_empty() { + tx.try_send(()).ok(); } - }, - ) + util::extend_sorted( + &mut *pending_paths, + path_events, + usize::MAX, + |a, b| a.path.cmp(&b.path), + ); + } + }) } - })??; + })?; - self.registrations.lock().insert(path, registration_id); + global(|g| { + g.watcher + .lock() + .watch(path, notify::RecursiveMode::NonRecursive) + })??; Ok(()) } fn remove(&self, path: &std::path::Path) -> anyhow::Result<()> { - let Some(registration) = self.registrations.lock().remove(path) else { - return Ok(()); - }; - - global(|w| w.remove(registration)) + use notify::Watcher; + Ok(global(|w| w.watcher.lock().unwatch(path))??) } } -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub struct WatcherRegistrationId(u32); - -struct WatcherRegistrationState { - callback: Arc, - path: Arc, -} - -struct WatcherState { - watchers: HashMap, - path_registrations: HashMap, u32>, - last_registration: WatcherRegistrationId, -} - pub struct GlobalWatcher { - state: Mutex, - - // DANGER: never keep the state lock while holding the watcher lock // two mutexes because calling watcher.add triggers an watcher.event, which needs watchers. #[cfg(target_os = "linux")] - watcher: Mutex, + pub(super) watcher: Mutex, #[cfg(target_os = "freebsd")] - watcher: Mutex, + pub(super) watcher: Mutex, #[cfg(target_os = "windows")] - watcher: Mutex, + pub(super) watcher: Mutex, + pub(super) watchers: Mutex>>, } impl GlobalWatcher { - #[must_use] - fn add( - &self, - path: Arc, - mode: notify::RecursiveMode, - cb: impl Fn(¬ify::Event) + Send + Sync + 'static, - ) -> anyhow::Result { - use notify::Watcher; - - self.watcher.lock().watch(&path, mode)?; - - let mut state = self.state.lock(); - - let id = state.last_registration; - state.last_registration = WatcherRegistrationId(id.0 + 1); - - let registration_state = WatcherRegistrationState { - callback: Arc::new(cb), - path: path.clone(), - }; - state.watchers.insert(id, registration_state); - *state.path_registrations.entry(path).or_insert(0) += 1; - - Ok(id) - } - - pub fn remove(&self, id: WatcherRegistrationId) { - use notify::Watcher; - let mut state = self.state.lock(); - let Some(registration_state) = state.watchers.remove(&id) else { - return; - }; - - let Some(count) = state.path_registrations.get_mut(®istration_state.path) else { - return; - }; - *count -= 1; - if *count == 0 { - state.path_registrations.remove(®istration_state.path); - - drop(state); - self.watcher - .lock() - .unwatch(®istration_state.path) - .log_err(); - } + pub(super) fn add(&self, cb: impl Fn(¬ify::Event) + Send + Sync + 'static) { + self.watchers.lock().push(Box::new(cb)) } } @@ -200,16 +114,8 @@ fn handle_event(event: Result) { return; }; global::<()>(move |watcher| { - let callbacks = { - let state = watcher.state.lock(); - state - .watchers - .values() - .map(|r| r.callback.clone()) - .collect::>() - }; - for callback in callbacks { - callback(&event); + for f in watcher.watchers.lock().iter() { + f(&event) } }) .log_err(); @@ -218,12 +124,8 @@ fn handle_event(event: Result) { pub fn global(f: impl FnOnce(&GlobalWatcher) -> T) -> anyhow::Result { let result = FS_WATCHER_INSTANCE.get_or_init(|| { notify::recommended_watcher(handle_event).map(|file_watcher| GlobalWatcher { - state: Mutex::new(WatcherState { - watchers: Default::default(), - path_registrations: Default::default(), - last_registration: Default::default(), - }), watcher: Mutex::new(file_watcher), + watchers: Default::default(), }) }); match result { diff --git a/crates/fs/src/mac_watcher.rs b/crates/fs/src/mac_watcher.rs index 7bd176639f..aa75ad31d9 100644 --- a/crates/fs/src/mac_watcher.rs +++ b/crates/fs/src/mac_watcher.rs @@ -41,9 +41,10 @@ impl Watcher for MacWatcher { if let Some((watched_path, _)) = handles .range::((Bound::Unbounded, Bound::Included(path))) .next_back() - && path.starts_with(watched_path) { - return Ok(()); + if path.starts_with(watched_path) { + return Ok(()); + } } let (stream, handle) = EventStream::new(&[path], self.latency); diff --git a/crates/fsevent/src/fsevent.rs b/crates/fsevent/src/fsevent.rs index c97ab5f35d..81ca0a4114 100644 --- a/crates/fsevent/src/fsevent.rs +++ b/crates/fsevent/src/fsevent.rs @@ -178,39 +178,40 @@ impl EventStream { flags.contains(StreamFlags::USER_DROPPED) || flags.contains(StreamFlags::KERNEL_DROPPED) }) - && let Some(last_valid_event_id) = state.last_valid_event_id.take() { - fs::FSEventStreamStop(state.stream); - fs::FSEventStreamInvalidate(state.stream); - fs::FSEventStreamRelease(state.stream); + 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, - ); + 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; + state.stream = stream; + fs::FSEventStreamScheduleWithRunLoop( + state.stream, + cf::CFRunLoopGetCurrent(), + cf::kCFRunLoopDefaultMode, + ); + fs::FSEventStreamStart(state.stream); + stream_restarted = true; + } } if !stream_restarted { diff --git a/crates/fuzzy/src/matcher.rs b/crates/fuzzy/src/matcher.rs index e649d47dd6..aff6390534 100644 --- a/crates/fuzzy/src/matcher.rs +++ b/crates/fuzzy/src/matcher.rs @@ -208,15 +208,8 @@ impl<'a> Matcher<'a> { return 1.0; } - let limit = self.last_positions[query_idx]; - let max_valid_index = (prefix.len() + path_lowercased.len()).saturating_sub(1); - let safe_limit = limit.min(max_valid_index); - - if path_idx > safe_limit { - return 0.0; - } - let path_len = prefix.len() + path.len(); + if let Some(memoized) = self.score_matrix[query_idx * path_len + path_idx] { return memoized; } @@ -225,13 +218,16 @@ impl<'a> Matcher<'a> { let mut best_position = 0; let query_char = self.lowercase_query[query_idx]; + let limit = self.last_positions[query_idx]; + + let max_valid_index = (prefix.len() + path_lowercased.len()).saturating_sub(1); + let safe_limit = limit.min(max_valid_index); let mut last_slash = 0; - for j in path_idx..=safe_limit { let extra_lowercase_chars_count = extra_lowercase_chars .iter() - .take_while(|&(&i, _)| i < j) + .take_while(|(i, _)| i < &&j) .map(|(_, increment)| increment) .sum::(); let j_regular = j - extra_lowercase_chars_count; @@ -240,9 +236,10 @@ impl<'a> Matcher<'a> { lowercase_prefix[j] } else { let path_index = j - prefix.len(); - match path_lowercased.get(path_index) { - Some(&char) => char, - None => continue, + if path_index < path_lowercased.len() { + path_lowercased[path_index] + } else { + continue; } }; let is_path_sep = path_char == MAIN_SEPARATOR; @@ -258,16 +255,18 @@ impl<'a> Matcher<'a> { #[cfg(target_os = "windows")] let need_to_score = query_char == path_char || (is_path_sep && query_char == '_'); if need_to_score { - let curr = match prefix.get(j_regular) { - Some(&curr) => curr, - None => path[j_regular - prefix.len()], + let curr = if j_regular < prefix.len() { + prefix[j_regular] + } else { + path[j_regular - prefix.len()] }; let mut char_score = 1.0; if j > path_idx { - let last = match prefix.get(j_regular - 1) { - Some(&last) => last, - None => path[j_regular - 1 - prefix.len()], + let last = if j_regular - 1 < prefix.len() { + prefix[j_regular - 1] + } else { + path[j_regular - 1 - prefix.len()] }; if last == MAIN_SEPARATOR { diff --git a/crates/git/Cargo.toml b/crates/git/Cargo.toml index 74656f1d4c..ab2210094d 100644 --- a/crates/git/Cargo.toml +++ b/crates/git/Cargo.toml @@ -12,7 +12,7 @@ workspace = true path = "src/git.rs" [features] -test-support = ["rand"] +test-support = [] [dependencies] anyhow.workspace = true @@ -26,7 +26,6 @@ http_client.workspace = true log.workspace = true parking_lot.workspace = true regex.workspace = true -rand = { workspace = true, optional = true } rope.workspace = true schemars.workspace = true serde.workspace = true @@ -48,4 +47,3 @@ text = { workspace = true, features = ["test-support"] } unindent.workspace = true gpui = { workspace = true, features = ["test-support"] } tempfile.workspace = true -rand.workspace = true diff --git a/crates/git/src/blame.rs b/crates/git/src/blame.rs index 24b2c44218..2128fa55c3 100644 --- a/crates/git/src/blame.rs +++ b/crates/git/src/blame.rs @@ -73,7 +73,6 @@ async fn run_git_blame( .current_dir(working_directory) .arg("blame") .arg("--incremental") - .arg("-w") .arg("--contents") .arg("-") .arg(path.as_os_str()) @@ -289,12 +288,14 @@ fn parse_git_blame(output: &str) -> Result> { } }; - if done && let Some(entry) = current_entry.take() { - index.insert(entry.sha, entries.len()); + if done { + if 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); + } } } } diff --git a/crates/git/src/git.rs b/crates/git/src/git.rs index e84014129c..553361e673 100644 --- a/crates/git/src/git.rs +++ b/crates/git/src/git.rs @@ -93,8 +93,6 @@ actions!( Init, /// Opens all modified files in the editor. OpenModifiedFiles, - /// Clones a repository. - Clone, ] ); @@ -119,13 +117,6 @@ impl Oid { Ok(Self(oid)) } - #[cfg(any(test, feature = "test-support"))] - pub fn random(rng: &mut impl rand::Rng) -> Self { - let mut bytes = [0; 20]; - rng.fill(&mut bytes); - Self::from_bytes(&bytes).unwrap() - } - pub fn as_bytes(&self) -> &[u8] { self.0.as_bytes() } diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index fd12dafa98..a63315e69e 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -6,7 +6,7 @@ use collections::HashMap; use futures::future::BoxFuture; use futures::{AsyncWriteExt, FutureExt as _, select_biased}; use git2::BranchType; -use gpui::{AppContext as _, AsyncApp, BackgroundExecutor, SharedString, Task}; +use gpui::{AppContext as _, AsyncApp, BackgroundExecutor, SharedString}; use parking_lot::Mutex; use rope::Rope; use schemars::JsonSchema; @@ -269,8 +269,10 @@ 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; @@ -336,7 +338,7 @@ pub trait GitRepository: Send + Sync { fn merge_message(&self) -> BoxFuture<'_, Option>; - fn status(&self, path_prefixes: &[RepoPath]) -> Task>; + fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<'_, Result>; fn branches(&self) -> BoxFuture<'_, Result>>; @@ -397,9 +399,9 @@ pub trait GitRepository: Send + Sync { &self, paths: Vec, env: Arc>, - ) -> BoxFuture<'_, Result<()>>; + ) -> BoxFuture>; - fn stash_pop(&self, env: Arc>) -> BoxFuture<'_, Result<()>>; + fn stash_pop(&self, env: Arc>) -> BoxFuture>; fn push( &self, @@ -461,8 +463,6 @@ pub trait GitRepository: Send + Sync { base_checkpoint: GitRepositoryCheckpoint, target_checkpoint: GitRepositoryCheckpoint, ) -> BoxFuture<'_, Result>; - - fn default_branch(&self) -> BoxFuture<'_, Result>>; } pub enum DiffType { @@ -844,19 +844,21 @@ impl GitRepository for RealGitRepository { .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn()?; - let mut stdin = child.stdin.take().unwrap(); - stdin.write_all(content.as_bytes()).await?; - stdin.flush().await?; - drop(stdin); + child + .stdin + .take() + .unwrap() + .write_all(content.as_bytes()) + .await?; let output = child.output().await?.stdout; - let sha = str::from_utf8(&output)?.trim(); + let sha = String::from_utf8(output)?; log::debug!("indexing SHA: {sha}, path {path:?}"); 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?; @@ -867,7 +869,6 @@ impl GitRepository for RealGitRepository { String::from_utf8_lossy(&output.stderr) ); } else { - log::debug!("removing path {path:?} from the index"); let output = new_smol_command(&git_binary_path) .current_dir(&working_directory) .envs(env.iter()) @@ -916,9 +917,8 @@ impl GitRepository for RealGitRepository { .context("no stdin for git cat-file subprocess")?; let mut stdin = BufWriter::new(stdin); for rev in &revs { - writeln!(&mut stdin, "{rev}")?; + write!(&mut stdin, "{rev}\n")?; } - stdin.flush()?; drop(stdin); let output = process.wait_with_output()?; @@ -951,27 +951,25 @@ impl GitRepository for RealGitRepository { .boxed() } - fn status(&self, path_prefixes: &[RepoPath]) -> Task> { + fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<'_, Result> { let git_binary_path = self.git_binary_path.clone(); - let working_directory = match self.working_directory() { - Ok(working_directory) => working_directory, - Err(e) => return Task::ready(Err(e)), - }; - 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) - .current_dir(working_directory) - .args(args) - .output()?; - if output.status.success() { - let stdout = String::from_utf8_lossy(&output.stdout); - stdout.parse() - } else { - let stderr = String::from_utf8_lossy(&output.stderr); - anyhow::bail!("git status failed: {stderr}"); - } - }) + let working_directory = self.working_directory(); + let path_prefixes = path_prefixes.to_owned(); + self.executor + .spawn(async move { + let output = new_std_command(&git_binary_path) + .current_dir(working_directory?) + .args(git_status_args(&path_prefixes)) + .output()?; + if output.status.success() { + let stdout = String::from_utf8_lossy(&output.stdout); + stdout.parse() + } else { + let stderr = String::from_utf8_lossy(&output.stderr); + anyhow::bail!("git status failed: {stderr}"); + } + }) + .boxed() } fn branches(&self) -> BoxFuture<'_, Result>> { @@ -1054,7 +1052,7 @@ impl GitRepository for RealGitRepository { let (_, branch_name) = name.split_once("/").context("Unexpected branch format")?; let revision = revision.get(); let branch_commit = revision.peel_to_commit()?; - let mut branch = repo.branch(branch_name, &branch_commit, false)?; + let mut branch = repo.branch(&branch_name, &branch_commit, false)?; branch.set_upstream(Some(&name))?; branch } else { @@ -1203,7 +1201,7 @@ impl GitRepository for RealGitRepository { &self, paths: Vec, env: Arc>, - ) -> BoxFuture<'_, Result<()>> { + ) -> BoxFuture> { let working_directory = self.working_directory(); self.executor .spawn(async move { @@ -1227,7 +1225,7 @@ impl GitRepository for RealGitRepository { .boxed() } - fn stash_pop(&self, env: Arc>) -> BoxFuture<'_, Result<()>> { + fn stash_pop(&self, env: Arc>) -> BoxFuture> { let working_directory = self.working_directory(); self.executor .spawn(async move { @@ -1445,11 +1443,12 @@ 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 - && merge_base.trim() == head - && 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 { + if merge_base.trim() == head { + if let Some(s) = remote_head.strip_prefix("refs/remotes/") { + remote_branches.push(s.to_owned().into()); + } + } } }; @@ -1571,9 +1570,10 @@ impl GitRepository for RealGitRepository { Err(error) => { if let Some(GitBinaryCommandError { status, .. }) = error.downcast_ref::() - && status.code() == Some(1) { - return Ok(false); + if status.code() == Some(1) { + return Ok(false); + } } Err(error) @@ -1607,37 +1607,6 @@ impl GitRepository for RealGitRepository { }) .boxed() } - - fn default_branch(&self) -> BoxFuture<'_, Result>> { - let working_directory = self.working_directory(); - let git_binary_path = self.git_binary_path.clone(); - - let executor = self.executor.clone(); - self.executor - .spawn(async move { - let working_directory = working_directory?; - let git = GitBinary::new(git_binary_path, working_directory, executor); - - if let Ok(output) = git - .run(&["symbolic-ref", "refs/remotes/upstream/HEAD"]) - .await - { - let output = output - .strip_prefix("refs/remotes/upstream/") - .map(|s| SharedString::from(s.to_owned())); - return Ok(output); - } - - let output = git - .run(&["symbolic-ref", "refs/remotes/origin/HEAD"]) - .await?; - - Ok(output - .strip_prefix("refs/remotes/origin/") - .map(|s| SharedString::from(s.to_owned()))) - }) - .boxed() - } } fn git_status_args(path_prefixes: &[RepoPath]) -> Vec { @@ -2028,7 +1997,7 @@ fn parse_branch_input(input: &str) -> Result> { branches.push(Branch { is_head: is_current_branch, - ref_name, + ref_name: ref_name, most_recent_commit: Some(CommitSummary { sha: head_sha, subject, @@ -2050,7 +2019,7 @@ fn parse_branch_input(input: &str) -> Result> { } fn parse_upstream_track(upstream_track: &str) -> Result { - if upstream_track.is_empty() { + if upstream_track == "" { return Ok(UpstreamTracking::Tracked(UpstreamTrackingStatus { ahead: 0, behind: 0, @@ -2345,7 +2314,7 @@ mod tests { #[allow(clippy::octal_escapes)] let input = "*\0060964da10574cd9bf06463a53bf6e0769c5c45e\0\0refs/heads/zed-patches\0refs/remotes/origin/zed-patches\0\01733187470\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(), diff --git a/crates/git/src/status.rs b/crates/git/src/status.rs index 71ca14c5b2..6158b51798 100644 --- a/crates/git/src/status.rs +++ b/crates/git/src/status.rs @@ -153,11 +153,17 @@ impl FileStatus { } pub fn is_conflicted(self) -> bool { - matches!(self, FileStatus::Unmerged { .. }) + match self { + FileStatus::Unmerged { .. } => true, + _ => false, + } } pub fn is_ignored(self) -> bool { - matches!(self, FileStatus::Ignored) + match self { + FileStatus::Ignored => true, + _ => false, + } } pub fn has_changes(&self) -> bool { @@ -170,31 +176,40 @@ impl FileStatus { pub fn is_modified(self) -> bool { match self { - FileStatus::Tracked(tracked) => matches!( - (tracked.index_status, tracked.worktree_status), - (StatusCode::Modified, _) | (_, StatusCode::Modified) - ), + FileStatus::Tracked(tracked) => match (tracked.index_status, tracked.worktree_status) { + (StatusCode::Modified, _) | (_, StatusCode::Modified) => true, + _ => false, + }, _ => false, } } pub fn is_created(self) -> bool { match self { - FileStatus::Tracked(tracked) => matches!( - (tracked.index_status, tracked.worktree_status), - (StatusCode::Added, _) | (_, StatusCode::Added) - ), + FileStatus::Tracked(tracked) => match (tracked.index_status, tracked.worktree_status) { + (StatusCode::Added, _) | (_, StatusCode::Added) => true, + _ => false, + }, FileStatus::Untracked => true, _ => false, } } pub fn is_deleted(self) -> bool { - matches!(self, FileStatus::Tracked(tracked) if matches!((tracked.index_status, tracked.worktree_status), (StatusCode::Deleted, _) | (_, StatusCode::Deleted))) + match self { + FileStatus::Tracked(tracked) => match (tracked.index_status, tracked.worktree_status) { + (StatusCode::Deleted, _) | (_, StatusCode::Deleted) => true, + _ => false, + }, + _ => false, + } } pub fn is_untracked(self) -> bool { - matches!(self, FileStatus::Untracked) + match self { + FileStatus::Untracked => true, + _ => false, + } } pub fn summary(self) -> GitSummary { @@ -453,7 +468,7 @@ impl FromStr for GitStatus { Some((path, status)) }) .collect::>(); - 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`. diff --git a/crates/git_hosting_providers/src/git_hosting_providers.rs b/crates/git_hosting_providers/src/git_hosting_providers.rs index 1d88c47f2e..b31412ed4a 100644 --- a/crates/git_hosting_providers/src/git_hosting_providers.rs +++ b/crates/git_hosting_providers/src/git_hosting_providers.rs @@ -49,13 +49,13 @@ pub fn register_additional_providers( pub fn get_host_from_git_remote_url(remote_url: &str) -> Result { maybe!({ - 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()); + 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()); + } } - Url::parse(remote_url) + Url::parse(&remote_url) .ok() .and_then(|remote_url| remote_url.host_str().map(|host| host.to_string())) }) diff --git a/crates/git_hosting_providers/src/providers/bitbucket.rs b/crates/git_hosting_providers/src/providers/bitbucket.rs index 26df7b567a..074a169135 100644 --- a/crates/git_hosting_providers/src/providers/bitbucket.rs +++ b/crates/git_hosting_providers/src/providers/bitbucket.rs @@ -1,22 +1,12 @@ use std::str::FromStr; -use std::sync::LazyLock; -use regex::Regex; use url::Url; use git::{ BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote, - PullRequest, RemoteUrl, + RemoteUrl, }; -fn pull_request_regex() -> &'static Regex { - static PULL_REQUEST_REGEX: LazyLock = LazyLock::new(|| { - // This matches Bitbucket PR reference pattern: (pull request #xxx) - Regex::new(r"\(pull request #(\d+)\)").unwrap() - }); - &PULL_REQUEST_REGEX -} - pub struct Bitbucket { name: String, base_url: Url, @@ -106,22 +96,6 @@ impl GitHostingProvider for Bitbucket { ); permalink } - - fn extract_pull_request(&self, remote: &ParsedGitRemote, message: &str) -> Option { - // Check first line of commit message for PR references - let first_line = message.lines().next()?; - - // Try to match against our PR patterns - let capture = pull_request_regex().captures(first_line)?; - let number = capture.get(1)?.as_str().parse::().ok()?; - - // Construct the PR URL in Bitbucket format - let mut url = self.base_url(); - let path = format!("/{}/{}/pull-requests/{}", remote.owner, remote.repo, number); - url.set_path(&path); - - Some(PullRequest { number, url }) - } } #[cfg(test)] @@ -229,34 +203,4 @@ mod tests { "https://bitbucket.org/zed-industries/zed/src/f00b4r/main.rs#lines-24:48"; assert_eq!(permalink.to_string(), expected_url.to_string()) } - - #[test] - fn test_bitbucket_pull_requests() { - use indoc::indoc; - - let remote = ParsedGitRemote { - owner: "zed-industries".into(), - repo: "zed".into(), - }; - - let bitbucket = Bitbucket::public_instance(); - - // Test message without PR reference - let message = "This does not contain a pull request"; - assert!(bitbucket.extract_pull_request(&remote, message).is_none()); - - // Pull request number at end of first line - let message = indoc! {r#" - Merged in feature-branch (pull request #123) - - Some detailed description of the changes. - "#}; - - let pr = bitbucket.extract_pull_request(&remote, message).unwrap(); - assert_eq!(pr.number, 123); - assert_eq!( - pr.url.as_str(), - "https://bitbucket.org/zed-industries/zed/pull-requests/123" - ); - } } diff --git a/crates/git_hosting_providers/src/providers/chromium.rs b/crates/git_hosting_providers/src/providers/chromium.rs index 5d940fb496..b68c629ec7 100644 --- a/crates/git_hosting_providers/src/providers/chromium.rs +++ b/crates/git_hosting_providers/src/providers/chromium.rs @@ -292,7 +292,7 @@ mod tests { assert_eq!( Chromium - .extract_pull_request(&remote, message) + .extract_pull_request(&remote, &message) .unwrap() .url .as_str(), diff --git a/crates/git_hosting_providers/src/providers/github.rs b/crates/git_hosting_providers/src/providers/github.rs index 4475afeb49..30f8d058a7 100644 --- a/crates/git_hosting_providers/src/providers/github.rs +++ b/crates/git_hosting_providers/src/providers/github.rs @@ -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); } } diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index 35f7a60354..4c919249ee 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -23,6 +23,7 @@ askpass.workspace = true buffer_diff.workspace = true call.workspace = true chrono.workspace = true +client.workspace = true cloud_llm_client.workspace = true collections.workspace = true command_palette_hooks.workspace = true @@ -70,7 +71,6 @@ windows.workspace = true ctor.workspace = true editor = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] } -indoc.workspace = true pretty_assertions.workspace = true project = { workspace = true, features = ["test-support"] } settings = { workspace = true, features = ["test-support"] } diff --git a/crates/git_ui/src/blame_ui.rs b/crates/git_ui/src/blame_ui.rs index 2768e3dc68..f910de7bbe 100644 --- a/crates/git_ui/src/blame_ui.rs +++ b/crates/git_ui/src/blame_ui.rs @@ -172,7 +172,7 @@ impl BlameRenderer for GitBlameRenderer { .clone() .unwrap_or("".to_string()) .into(), - author_email: blame.author_mail.unwrap_or("".to_string()).into(), + author_email: blame.author_mail.clone().unwrap_or("".to_string()).into(), message: details, }; @@ -186,7 +186,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(); + let full_sha = commit_details.sha.to_string().clone(); let absolute_timestamp = format_local_timestamp( commit_details.commit_time, OffsetDateTime::now_utc(), @@ -377,7 +377,7 @@ impl BlameRenderer for GitBlameRenderer { has_parent: true, }, repository.downgrade(), - workspace, + workspace.clone(), window, cx, ) diff --git a/crates/git_ui/src/branch_picker.rs b/crates/git_ui/src/branch_picker.rs index fb56cdcc5d..9eac3ce5af 100644 --- a/crates/git_ui/src/branch_picker.rs +++ b/crates/git_ui/src/branch_picker.rs @@ -13,7 +13,7 @@ use project::git_store::Repository; use std::sync::Arc; use time::OffsetDateTime; use time_format::format_local_timestamp; -use ui::{HighlightedLabel, ListItem, ListItemSpacing, Tooltip, prelude::*}; +use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*}; use util::ResultExt; use workspace::notifications::DetachAndPromptErr; use workspace::{ModalView, Workspace}; @@ -48,7 +48,7 @@ pub fn open( window: &mut Window, cx: &mut Context, ) { - let repository = workspace.project().read(cx).active_repository(cx); + let repository = workspace.project().read(cx).active_repository(cx).clone(); let style = BranchListStyle::Modal; workspace.toggle_modal(window, cx, |window, cx| { BranchList::new(repository, style, rems(34.), window, cx) @@ -90,21 +90,11 @@ impl BranchList { let all_branches_request = repository .clone() .map(|repository| repository.update(cx, |repository, _| repository.branches())); - let default_branch_request = repository - .clone() - .map(|repository| repository.update(cx, |repository, _| repository.default_branch())); cx.spawn_in(window, async move |this, cx| { let mut all_branches = all_branches_request .context("No active repository")? .await??; - let default_branch = default_branch_request - .context("No active repository")? - .await - .map(Result::ok) - .ok() - .flatten() - .flatten(); let all_branches = cx .background_spawn(async move { @@ -134,7 +124,6 @@ impl BranchList { this.update_in(cx, |this, window, cx| { this.picker.update(cx, |picker, cx| { - picker.delegate.default_branch = default_branch; picker.delegate.all_branches = Some(all_branches); picker.refresh(window, cx); }) @@ -144,7 +133,7 @@ impl BranchList { }) .detach_and_log_err(cx); - let delegate = BranchListDelegate::new(repository, style); + let delegate = BranchListDelegate::new(repository.clone(), style); let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); let _subscription = cx.subscribe(&picker, |_, _, _, cx| { @@ -180,7 +169,6 @@ impl Focusable for BranchList { impl Render for BranchList { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() - .key_context("GitBranchSelector") .w(self.width) .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed)) .child(self.picker.clone()) @@ -204,7 +192,6 @@ struct BranchEntry { pub struct BranchListDelegate { matches: Vec, all_branches: Option>, - default_branch: Option, repo: Option>, style: BranchListStyle, selected_index: usize, @@ -219,7 +206,6 @@ impl BranchListDelegate { repo, style, all_branches: None, - default_branch: None, selected_index: 0, last_query: Default::default(), modifiers: Default::default(), @@ -228,7 +214,6 @@ impl BranchListDelegate { fn create_branch( &self, - from_branch: Option, new_branch_name: SharedString, window: &mut Window, cx: &mut Context>, @@ -238,11 +223,6 @@ impl BranchListDelegate { }; let new_branch_name = new_branch_name.to_string().replace(' ', "-"); cx.spawn(async move |_, cx| { - if let Some(based_branch) = from_branch { - repo.update(cx, |repo, _| repo.change_branch(based_branch.to_string()))? - .await??; - } - repo.update(cx, |repo, _| { repo.create_branch(new_branch_name.to_string()) })? @@ -373,22 +353,12 @@ impl PickerDelegate for BranchListDelegate { }) } - fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context>) { + fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context>) { let Some(entry) = self.matches.get(self.selected_index()) else { return; }; if entry.is_new { - let from_branch = if secondary { - self.default_branch.clone() - } else { - None - }; - self.create_branch( - from_branch, - entry.branch.name().to_owned().into(), - window, - cx, - ); + self.create_branch(entry.branch.name().to_owned().into(), window, cx); return; } @@ -469,28 +439,6 @@ impl PickerDelegate for BranchListDelegate { }) .unwrap_or_else(|| (None, None)); - let icon = if let Some(default_branch) = self.default_branch.clone() - && entry.is_new - { - Some( - IconButton::new("branch-from-default", IconName::GitBranchAlt) - .on_click(cx.listener(move |this, _, window, cx| { - this.delegate.set_selected_index(ix, window, cx); - this.delegate.confirm(true, window, cx); - })) - .tooltip(move |window, cx| { - Tooltip::for_action( - format!("Create branch based off default: {default_branch}"), - &menu::SecondaryConfirm, - window, - cx, - ) - }), - ) - } else { - None - }; - let branch_name = if entry.is_new { h_flex() .gap_1() @@ -556,8 +504,7 @@ impl PickerDelegate for BranchListDelegate { .color(Color::Muted) })) }), - ) - .end_slot::(icon), + ), ) } diff --git a/crates/git_ui/src/commit_modal.rs b/crates/git_ui/src/commit_modal.rs index cae4d28a83..88ec2dc84e 100644 --- a/crates/git_ui/src/commit_modal.rs +++ b/crates/git_ui/src/commit_modal.rs @@ -1,9 +1,9 @@ use crate::branch_picker::{self, BranchList}; use crate::git_panel::{GitPanel, commit_message_editor}; +use client::DisableAiSettings; use git::repository::CommitOptions; use git::{Amend, Commit, GenerateCommitMessage, Signoff}; use panel::{panel_button, panel_editor_style}; -use project::DisableAiSettings; use settings::Settings; use ui::{ ContextMenu, KeybindingHint, PopoverMenu, PopoverMenuHandle, SplitButton, Tooltip, prelude::*, @@ -35,7 +35,7 @@ impl ModalContainerProperties { // Calculate width based on character width let mut modal_width = 460.0; - let style = window.text_style(); + let style = window.text_style().clone(); let font_id = window.text_system().resolve_font(&style.font()); let font_size = style.font_size.to_pixels(window.rem_size()); @@ -135,10 +135,11 @@ impl CommitModal { .as_ref() .and_then(|repo| repo.read(cx).head_commit.as_ref()) .is_some() - && !git_panel.amend_pending() { - git_panel.set_amend_pending(true, cx); - git_panel.load_last_commit_message_if_empty(cx); + if !git_panel.amend_pending() { + git_panel.set_amend_pending(true, cx); + git_panel.load_last_commit_message_if_empty(cx); + } } } ForceMode::Commit => { @@ -179,7 +180,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); + let buffer = git_panel.commit_message_buffer(cx).clone(); let panel_editor = git_panel.commit_editor.clone(); let project = git_panel.project.clone(); @@ -194,12 +195,12 @@ impl CommitModal { let commit_message = commit_editor.read(cx).text(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, 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); + }); + } } let focus_handle = commit_editor.focus_handle(cx); @@ -271,7 +272,7 @@ impl CommitModal { .child( div() .px_1() - .child(Icon::new(IconName::ChevronDown).size(IconSize::XSmall)), + .child(Icon::new(IconName::ChevronDownSmall).size(IconSize::XSmall)), ), ) .menu({ @@ -285,7 +286,7 @@ impl CommitModal { Some(ContextMenu::build(window, cx, |context_menu, _, _| { context_menu .when_some(keybinding_target.clone(), |el, keybinding_target| { - el.context(keybinding_target) + el.context(keybinding_target.clone()) }) .when(has_previous_commit, |this| { this.toggleable_entry( @@ -391,9 +392,15 @@ impl CommitModal { }); let focus_handle = self.focus_handle(cx); - 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") - }); + 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 + }; h_flex() .group("commit_editor_footer") @@ -476,7 +483,7 @@ impl CommitModal { }), self.render_git_commit_menu( ElementId::Name(format!("split-button-right-{}", commit_label).into()), - Some(focus_handle), + Some(focus_handle.clone()), ) .into_any_element(), )), diff --git a/crates/git_ui/src/commit_tooltip.rs b/crates/git_ui/src/commit_tooltip.rs index a470bc6925..00ab911610 100644 --- a/crates/git_ui/src/commit_tooltip.rs +++ b/crates/git_ui/src/commit_tooltip.rs @@ -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(); + let full_sha = self.commit.sha.to_string().clone(); let absolute_timestamp = format_local_timestamp( self.commit.commit_time, OffsetDateTime::now_utc(), diff --git a/crates/git_ui/src/commit_view.rs b/crates/git_ui/src/commit_view.rs index d428ccbb05..c8c237fe90 100644 --- a/crates/git_ui/src/commit_view.rs +++ b/crates/git_ui/src/commit_view.rs @@ -88,10 +88,11 @@ impl CommitView { let ix = pane.items().position(|item| { let commit_view = item.downcast::(); commit_view - .is_some_and(|view| view.read(cx).commit.sha == commit.sha) + .map_or(false, |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); } @@ -159,7 +160,7 @@ impl CommitView { }); } - cx.spawn(async move |this, cx| { + cx.spawn(async move |this, mut cx| { for file in commit_diff.files { let is_deleted = file.new_text.is_none(); let new_text = file.new_text.unwrap_or_default(); @@ -178,9 +179,9 @@ impl CommitView { worktree_id, }) as Arc; - let buffer = build_buffer(new_text, file, &language_registry, cx).await?; + let buffer = build_buffer(new_text, file, &language_registry, &mut cx).await?; let buffer_diff = - build_buffer_diff(old_text, &buffer, &language_registry, cx).await?; + build_buffer_diff(old_text, &buffer, &language_registry, &mut cx).await?; this.update(cx, |this, cx| { this.multibuffer.update(cx, |multibuffer, cx| { diff --git a/crates/git_ui/src/conflict_view.rs b/crates/git_ui/src/conflict_view.rs index ee1b82920d..0bbb9411be 100644 --- a/crates/git_ui/src/conflict_view.rs +++ b/crates/git_ui/src/conflict_view.rs @@ -55,7 +55,7 @@ pub fn register_editor(editor: &mut Editor, buffer: Entity, cx: &mu buffers: Default::default(), }); - let buffers = buffer.read(cx).all_buffers(); + let buffers = buffer.read(cx).all_buffers().clone(); 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, cx: &mut Context) { - 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, cx: &mut Context( 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())) } @@ -426,7 +428,7 @@ impl GitPanel { let git_store = project.read(cx).git_store().clone(); let active_repository = project.read(cx).active_repository(cx); - cx.new(|cx| { + let git_panel = 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| { @@ -563,7 +565,9 @@ impl GitPanel { this.schedule_update(false, window, cx); this - }) + }); + + git_panel } fn hide_scrollbars(&mut self, window: &mut Window, cx: &mut Context) { @@ -650,14 +654,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); } @@ -669,7 +673,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); } @@ -685,7 +689,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); } @@ -773,7 +777,7 @@ impl GitPanel { if window .focused(cx) - .is_some_and(|focused| self.focus_handle == focused) + .map_or(false, |focused| self.focus_handle == focused) { dispatch_context.add("menu"); dispatch_context.add("ChangesList"); @@ -892,7 +896,9 @@ impl GitPanel { let have_entries = self .active_repository .as_ref() - .is_some_and(|active_repository| active_repository.read(cx).status_summary().count > 0); + .map_or(false, |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); @@ -922,17 +928,19 @@ 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::(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; + if let Some(project_diff) = workspace.read(cx).active_item_as::(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; + } + } }; self.workspace @@ -1196,13 +1204,16 @@ impl GitPanel { window, cx, ); - cx.spawn(async move |this, cx| { - if let Ok(RestoreCancel::RestoreTrackedFiles) = prompt.await { + cx.spawn(async move |this, cx| match prompt.await { + Ok(RestoreCancel::RestoreTrackedFiles) => { this.update(cx, |this, cx| { this.perform_checkout(entries, cx); }) .ok(); } + _ => { + return; + } }) .detach(); } @@ -1332,10 +1343,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) }) - .cloned() + .map(|status_entry| status_entry.clone()) .collect::>(); (goal_staged_state, entries) @@ -1467,6 +1478,7 @@ impl GitPanel { .read(cx) .as_singleton() .unwrap() + .clone() } fn toggle_staged_for_selected( @@ -1632,12 +1644,13 @@ impl GitPanel { fn has_commit_message(&self, cx: &mut Context) -> bool { let text = self.commit_editor.read(cx).text(cx); if !text.trim().is_empty() { - true + return true; } else if text.is_empty() { - self.suggest_commit_message(cx) - .is_some_and(|text| !text.trim().is_empty()) + return self + .suggest_commit_message(cx) + .is_some_and(|text| !text.trim().is_empty()); } else { - false + return false; } } @@ -1822,9 +1835,7 @@ impl GitPanel { let git_status_entry = if let Some(staged_entry) = &self.single_staged_entry { Some(staged_entry) - } else if self.total_staged_count() == 0 - && let Some(single_tracked_entry) = &self.single_tracked_entry - { + } else if let Some(single_tracked_entry) = &self.single_tracked_entry { Some(single_tracked_entry) } else { None @@ -1941,7 +1952,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 { @@ -2072,100 +2083,6 @@ impl GitPanel { .detach_and_log_err(cx); } - pub(crate) fn git_clone(&mut self, repo: String, window: &mut Window, cx: &mut Context) { - let path = cx.prompt_for_paths(gpui::PathPromptOptions { - files: false, - directories: true, - multiple: false, - prompt: Some("Select as Repository Destination".into()), - }); - - let workspace = self.workspace.clone(); - - cx.spawn_in(window, async move |this, cx| { - let mut paths = path.await.ok()?.ok()??; - let mut path = paths.pop()?; - let repo_name = repo - .split(std::path::MAIN_SEPARATOR_STR) - .last()? - .strip_suffix(".git")? - .to_owned(); - - let fs = this.read_with(cx, |this, _| this.fs.clone()).ok()?; - - let prompt_answer = match fs.git_clone(&repo, path.as_path()).await { - Ok(_) => cx.update(|window, cx| { - window.prompt( - PromptLevel::Info, - &format!("Git Clone: {}", repo_name), - None, - &["Add repo to project", "Open repo in new project"], - cx, - ) - }), - Err(e) => { - this.update(cx, |this: &mut GitPanel, cx| { - let toast = StatusToast::new(e.to_string(), cx, |this, _| { - this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error)) - .dismiss_button(true) - }); - - this.workspace - .update(cx, |workspace, cx| { - workspace.toggle_status_toast(toast, cx); - }) - .ok(); - }) - .ok()?; - - return None; - } - } - .ok()?; - - path.push(repo_name); - match prompt_answer.await.ok()? { - 0 => { - workspace - .update(cx, |workspace, cx| { - workspace - .project() - .update(cx, |project, cx| { - project.create_worktree(path.as_path(), true, cx) - }) - .detach(); - }) - .ok(); - } - 1 => { - workspace - .update(cx, move |workspace, cx| { - workspace::open_new( - Default::default(), - workspace.app_state().clone(), - cx, - move |workspace, _, cx| { - cx.activate(true); - workspace - .project() - .update(cx, |project, cx| { - project.create_worktree(&path, true, cx) - }) - .detach(); - }, - ) - .detach(); - }) - .ok(); - } - _ => {} - } - - Some(()) - }) - .detach(); - } - pub(crate) fn git_init(&mut self, window: &mut Window, cx: &mut Context) { let worktrees = self .project @@ -2175,7 +2092,7 @@ impl GitPanel { let worktree = if worktrees.len() == 1 { Task::ready(Some(worktrees.first().unwrap().clone())) - } else if worktrees.is_empty() { + } else if worktrees.len() == 0 { let result = window.prompt( PromptLevel::Warning, "Unable to initialize a git repository", @@ -2503,11 +2420,10 @@ impl GitPanel { new_co_authors.push((name.clone(), email.clone())) } } - if !project.is_local() - && !project.is_read_only(cx) - && let Some(local_committer) = self.local_committer(room, cx) - { - new_co_authors.push(local_committer); + 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); + } } new_co_authors } @@ -2746,34 +2662,35 @@ 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.first().cloned(); + last_pending_staged = pending.entries.iter().next().cloned(); } - if let Some(single_staged) = &single_staged_entry - && pending + if let Some(single_staged) = &single_staged_entry { + if 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.is_empty() && staged_count == 1 && pending_staged_count == 0 { + if conflict_entries.len() == 0 && 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.is_empty() && pending_staged_count == 1 { + } else if conflict_entries.len() == 0 && pending_staged_count == 1 { self.single_staged_entry = last_pending_staged; } - if conflict_entries.is_empty() && changed_entries.len() == 1 { + if conflict_entries.len() == 0 && changed_entries.len() == 1 { self.single_tracked_entry = changed_entries.first().cloned(); } - if !conflict_entries.is_empty() { + if conflict_entries.len() > 0 { self.entries.push(GitListEntry::Header(GitHeaderEntry { header: Section::Conflict, })); @@ -2781,7 +2698,7 @@ impl GitPanel { .extend(conflict_entries.into_iter().map(GitListEntry::Status)); } - if !changed_entries.is_empty() { + if changed_entries.len() > 0 { if !sort_by_path { self.entries.push(GitListEntry::Header(GitHeaderEntry { header: Section::Tracked, @@ -2790,7 +2707,7 @@ impl GitPanel { self.entries .extend(changed_entries.into_iter().map(GitListEntry::Status)); } - if !new_entries.is_empty() { + if new_entries.len() > 0 { self.entries.push(GitListEntry::Header(GitHeaderEntry { header: Section::New, })); @@ -2929,7 +2846,8 @@ impl GitPanel { .matches(git::repository::REMOTE_CANCELLED_BY_USER) .next() .is_some() - { // Hide the cancelled by user message + { + return; // Hide the cancelled by user message } else { workspace.update(cx, |workspace, cx| { let workspace_weak = cx.weak_entity(); @@ -2983,9 +2901,9 @@ 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, ToastWithLog { output } => this - .icon(ToastIcon::new(IconName::GitBranchAlt).color(Color::Muted)) + .icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted)) .action("View Log", move |window, cx| { let output = output.clone(); let output = @@ -2996,9 +2914,9 @@ impl GitPanel { }) .ok(); }), - PushPrLink { text, link } => this - .icon(ToastIcon::new(IconName::GitBranchAlt).color(Color::Muted)) - .action(text, move |_, cx| cx.open_url(&link)), + PushPrLink { link } => this + .icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted)) + .action("Open Pull Request", move |_, cx| cx.open_url(&link)), } }); workspace.toggle_status_toast(status_toast, cx) @@ -3191,7 +3109,7 @@ impl GitPanel { .justify_center() .border_l_1() .border_color(cx.theme().colors().border) - .child(Icon::new(IconName::ChevronDown).size(IconSize::XSmall)), + .child(Icon::new(IconName::ChevronDownSmall).size(IconSize::XSmall)), ), ) .menu({ @@ -3204,7 +3122,7 @@ impl GitPanel { Some(ContextMenu::build(window, cx, |context_menu, _, _| { context_menu .when_some(keybinding_target.clone(), |el, keybinding_target| { - el.context(keybinding_target) + el.context(keybinding_target.clone()) }) .when(has_previous_commit, |this| { this.toggleable_entry( @@ -3260,10 +3178,12 @@ impl GitPanel { } else { "Amend Tracked" } - } else if self.has_staged_changes() { - "Commit" } else { - "Commit Tracked" + if self.has_staged_changes() { + "Commit" + } else { + "Commit Tracked" + } } } @@ -3384,7 +3304,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; + let expand_tooltip_focus_handle = editor_focus_handle.clone(); let branch = active_repository.read(cx).branch.clone(); let head_commit = active_repository.read(cx).head_commit.clone(); @@ -3397,7 +3317,7 @@ impl GitPanel { * MAX_PANEL_EDITOR_LINES + gap; - let git_panel = cx.entity(); + let git_panel = cx.entity().clone(); let display_name = SharedString::from(Arc::from( active_repository .read(cx) @@ -3413,7 +3333,7 @@ impl GitPanel { display_name, branch, head_commit, - Some(git_panel), + Some(git_panel.clone()), )) .child( panel_editor_container(window, cx) @@ -3564,7 +3484,7 @@ impl GitPanel { }), self.render_git_commit_menu( ElementId::Name(format!("split-button-right-{}", title).into()), - Some(commit_tooltip_focus_handle), + Some(commit_tooltip_focus_handle.clone()), cx, ) .into_any_element(), @@ -3630,7 +3550,7 @@ impl GitPanel { CommitView::open( commit.clone(), repo.clone(), - workspace.clone(), + workspace.clone().clone(), window, cx, ); @@ -4338,7 +4258,7 @@ impl GitPanel { } }) .child( - self.entry_label(display_name, label_color) + self.entry_label(display_name.clone(), label_color) .when(status.is_deleted(), |this| this.strikethrough()), ), ) @@ -4476,7 +4396,7 @@ fn current_language_model(cx: &Context<'_, GitPanel>) -> Option) -> impl IntoElement { let project = self.project.read(cx); - let has_entries = !self.entries.is_empty(); + let has_entries = self.entries.len() > 0; let room = self .workspace .upgrade() @@ -4484,7 +4404,7 @@ impl Render for GitPanel { let has_write_access = self.has_write_access(cx); - let has_co_authors = room.is_some_and(|room| { + let has_co_authors = room.map_or(false, |room| { self.load_local_committer(cx); let room = room.read(cx); room.remote_participants() @@ -4604,7 +4524,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) } } @@ -4641,7 +4561,7 @@ impl Panel for GitPanel { } fn icon(&self, _: &Window, cx: &App) -> Option { - Some(ui::IconName::GitBranchAlt).filter(|_| GitPanelSettings::get_global(cx).button) + Some(ui::IconName::GitBranchSmall).filter(|_| GitPanelSettings::get_global(cx).button) } fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> { @@ -4687,7 +4607,7 @@ impl GitPanelMessageTooltip { author_email: details.author_email.clone(), commit_time: OffsetDateTime::from_unix_timestamp(details.commit_timestamp)?, message: Some(ParsedCommitMessage { - message: details.message, + message: details.message.clone(), ..Default::default() }), }; @@ -4800,10 +4720,12 @@ 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 { @@ -4811,7 +4733,8 @@ 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() @@ -4820,7 +4743,7 @@ impl RenderOnce for PanelRepoFooter { }; let truncated_branch_name = if branch_actual_len <= branch_display_len { - branch_name + branch_name.to_string() } else { util::truncate_and_trailoff(branch_name.trim_ascii(), branch_display_len) }; @@ -4833,7 +4756,7 @@ impl RenderOnce for PanelRepoFooter { let repo_selector = PopoverMenu::new("repository-switcher") .menu({ - let project = project; + let project = project.clone(); move |window, cx| { let project = project.clone()?; Some(cx.new(|cx| RepositorySelector::new(project, rems(16.), window, cx))) @@ -4885,7 +4808,7 @@ impl RenderOnce for PanelRepoFooter { .items_center() .child( div().child( - Icon::new(IconName::GitBranchAlt) + Icon::new(IconName::GitBranchSmall) .size(IconSize::Small) .color(if single_repo { Color::Disabled @@ -5004,7 +4927,10 @@ impl Component for PanelRepoFooter { div() .w(example_width) .overflow_hidden() - .child(PanelRepoFooter::new_preview(active_repository(1), None)) + .child(PanelRepoFooter::new_preview( + active_repository(1).clone(), + None, + )) .into_any_element(), ), single_example( @@ -5013,7 +4939,7 @@ impl Component for PanelRepoFooter { .w(example_width) .overflow_hidden() .child(PanelRepoFooter::new_preview( - active_repository(2), + active_repository(2).clone(), Some(branch(unknown_upstream)), )) .into_any_element(), @@ -5024,7 +4950,7 @@ impl Component for PanelRepoFooter { .w(example_width) .overflow_hidden() .child(PanelRepoFooter::new_preview( - active_repository(3), + active_repository(3).clone(), Some(branch(no_remote_upstream)), )) .into_any_element(), @@ -5035,7 +4961,7 @@ impl Component for PanelRepoFooter { .w(example_width) .overflow_hidden() .child(PanelRepoFooter::new_preview( - active_repository(4), + active_repository(4).clone(), Some(branch(not_ahead_or_behind_upstream)), )) .into_any_element(), @@ -5046,7 +4972,7 @@ impl Component for PanelRepoFooter { .w(example_width) .overflow_hidden() .child(PanelRepoFooter::new_preview( - active_repository(5), + active_repository(5).clone(), Some(branch(behind_upstream)), )) .into_any_element(), @@ -5057,7 +4983,7 @@ impl Component for PanelRepoFooter { .w(example_width) .overflow_hidden() .child(PanelRepoFooter::new_preview( - active_repository(6), + active_repository(6).clone(), Some(branch(ahead_of_upstream)), )) .into_any_element(), @@ -5068,7 +4994,7 @@ impl Component for PanelRepoFooter { .w(example_width) .overflow_hidden() .child(PanelRepoFooter::new_preview( - active_repository(7), + active_repository(7).clone(), Some(branch(ahead_and_behind_upstream)), )) .into_any_element(), @@ -5189,6 +5115,7 @@ mod tests { language::init(cx); editor::init(cx); Project::init_settings(cx); + client::DisableAiSettings::register(cx); crate::init(cx); }); } @@ -5239,7 +5166,7 @@ mod tests { project .read(cx) .worktrees(cx) - .next() + .nth(0) .unwrap() .read(cx) .as_local() @@ -5364,7 +5291,7 @@ mod tests { project .read(cx) .worktrees(cx) - .next() + .nth(0) .unwrap() .read(cx) .as_local() @@ -5415,7 +5342,7 @@ mod tests { project .read(cx) .worktrees(cx) - .next() + .nth(0) .unwrap() .read(cx) .as_local() @@ -5464,7 +5391,7 @@ mod tests { project .read(cx) .worktrees(cx) - .next() + .nth(0) .unwrap() .read(cx) .as_local() diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index 5369b8b404..0163175eda 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -10,17 +10,14 @@ use git::{ status::{FileStatus, StatusCode, UnmergedStatus, UnmergedStatusCode}, }; use git_panel_settings::GitPanelSettings; -use gpui::{ - Action, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Window, - actions, -}; +use gpui::{Action, App, Context, FocusHandle, Window, actions}; use onboarding::GitOnboardingModal; use project_diff::ProjectDiff; use ui::prelude::*; -use workspace::{ModalView, Workspace}; +use workspace::Workspace; use zed_actions; -use crate::{git_panel::GitPanel, text_diff_view::TextDiffView}; +use crate::text_diff_view::TextDiffView; mod askpass_modal; pub mod branch_picker; @@ -172,15 +169,6 @@ pub fn init(cx: &mut App) { panel.git_init(window, cx); }); }); - workspace.register_action(|workspace, _action: &git::Clone, window, cx| { - let Some(panel) = workspace.panel::(cx) else { - return; - }; - - workspace.toggle_modal(window, cx, |window, cx| { - GitCloneModal::show(panel, window, cx) - }); - }); workspace.register_action(|workspace, _: &git::OpenModifiedFiles, window, cx| { open_modified_files(workspace, window, cx); }); @@ -245,12 +233,12 @@ fn render_remote_button( } (0, 0) => None, (ahead, 0) => Some(remote_button::render_push_button( - keybinding_target, + keybinding_target.clone(), id, ahead, )), (ahead, behind) => Some(remote_button::render_pull_button( - keybinding_target, + keybinding_target.clone(), id, ahead, behind, @@ -368,7 +356,7 @@ mod remote_button { "Publish", 0, 0, - Some(IconName::ExpandUp), + Some(IconName::ArrowUpFromLine), keybinding_target.clone(), move |_, window, cx| { window.dispatch_action(Box::new(git::Push), cx); @@ -395,7 +383,7 @@ mod remote_button { "Republish", 0, 0, - Some(IconName::ExpandUp), + Some(IconName::ArrowUpFromLine), keybinding_target.clone(), move |_, window, cx| { window.dispatch_action(Box::new(git::Push), cx); @@ -425,9 +413,16 @@ mod remote_button { let command = command.into(); if let Some(handle) = focus_handle { - Tooltip::with_meta_in(label, Some(action), command, &handle, window, cx) + Tooltip::with_meta_in( + label.clone(), + Some(action), + command.clone(), + &handle, + window, + cx, + ) } else { - Tooltip::with_meta(label, Some(action), command, window, cx) + Tooltip::with_meta(label.clone(), Some(action), command.clone(), window, cx) } } @@ -443,14 +438,14 @@ mod remote_button { .child( div() .px_1() - .child(Icon::new(IconName::ChevronDown).size(IconSize::XSmall)), + .child(Icon::new(IconName::ChevronDownSmall).size(IconSize::XSmall)), ), ) .menu(move |window, cx| { Some(ContextMenu::build(window, cx, |context_menu, _, _| { context_menu .when_some(keybinding_target.clone(), |el, keybinding_target| { - el.context(keybinding_target) + el.context(keybinding_target.clone()) }) .action("Fetch", git::Fetch.boxed_clone()) .action("Fetch From", git::FetchFrom.boxed_clone()) @@ -618,88 +613,3 @@ impl Component for GitStatusIcon { ) } } - -struct GitCloneModal { - panel: Entity, - repo_input: Entity, - focus_handle: FocusHandle, -} - -impl GitCloneModal { - pub fn show(panel: Entity, window: &mut Window, cx: &mut Context) -> Self { - let repo_input = cx.new(|cx| { - let mut editor = Editor::single_line(window, cx); - editor.set_placeholder_text("Enter repository URL…", cx); - editor - }); - let focus_handle = repo_input.focus_handle(cx); - - window.focus(&focus_handle); - - Self { - panel, - repo_input, - focus_handle, - } - } -} - -impl Focusable for GitCloneModal { - fn focus_handle(&self, _: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl Render for GitCloneModal { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - div() - .elevation_3(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); - })) - .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| { - let repo = this.repo_input.read(cx).text(cx); - this.panel.update(cx, |panel, cx| { - panel.git_clone(repo, window, cx); - }); - cx.emit(DismissEvent); - })) - } -} - -impl EventEmitter for GitCloneModal {} - -impl ModalView for GitCloneModal {} diff --git a/crates/git_ui/src/onboarding.rs b/crates/git_ui/src/onboarding.rs index d1709e043b..d721b21a2a 100644 --- a/crates/git_ui/src/onboarding.rs +++ b/crates/git_ui/src/onboarding.rs @@ -110,7 +110,7 @@ impl Render for GitOnboardingModal { .child(Headline::new("Native Git Support").size(HeadlineSize::Large)), ) .child(h_flex().absolute().top_2().right_2().child( - IconButton::new("cancel", IconName::Close).on_click(cx.listener( + IconButton::new("cancel", IconName::X).on_click(cx.listener( |_, _: &ClickEvent, _window, cx| { git_onboarding_event!("Cancelled", trigger = "X click"); cx.emit(DismissEvent); diff --git a/crates/git_ui/src/picker_prompt.rs b/crates/git_ui/src/picker_prompt.rs index 3f1d507c42..4077e0f362 100644 --- a/crates/git_ui/src/picker_prompt.rs +++ b/crates/git_ui/src/picker_prompt.rs @@ -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::>() }); let Some(candidates) = candidates.log_err() else { diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index 524dbf13d3..d6a4e27286 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -242,7 +242,7 @@ impl ProjectDiff { TRACKED_NAMESPACE }; - let path_key = PathKey::namespaced(namespace, entry.repo_path.0); + let path_key = PathKey::namespaced(namespace, entry.repo_path.0.clone()); self.move_to_path(path_key, window, cx) } @@ -280,7 +280,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().nth(1).is_some(); + let prev_next = snapshot.diff_hunks().skip(1).next().is_some(); let mut selection = true; let mut ranges = editor @@ -329,14 +329,14 @@ impl ProjectDiff { }) .ok(); - ButtonStates { + return ButtonStates { stage: has_unstaged_hunks, unstage: has_staged_hunks, prev_next, selection, stage_all, unstage_all, - } + }; } fn handle_editor_event( @@ -346,24 +346,27 @@ impl ProjectDiff { window: &mut Window, cx: &mut Context, ) { - 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::(cx) { - git_panel.update(cx, |git_panel, cx| { - git_panel.select_entry_by_path(project_path, window, cx) - }) - } - }) - .ok(); + 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::(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) - && self.multibuffer.read(cx).is_empty() - { - self.focus_handle.focus(window) + if editor.focus_handle(cx).contains_focused(window, cx) { + if self.multibuffer.read(cx).is_empty() { + self.focus_handle.focus(window) + } } } @@ -448,10 +451,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); + .map(|diff_hunk| diff_hunk.buffer_range.clone()); let conflicts = conflict_addon .conflict_set(snapshot.remote_id()) - .map(|conflict_set| conflict_set.read(cx).snapshot().conflicts) + .map(|conflict_set| conflict_set.read(cx).snapshot().conflicts.clone()) .unwrap_or_default(); let conflicts = conflicts.iter().map(|conflict| conflict.range.clone()); @@ -510,7 +513,7 @@ impl ProjectDiff { mut recv: postage::watch::Receiver<()>, cx: &mut AsyncWindowContext, ) -> Result<()> { - while (recv.next().await).is_some() { + while let Some(_) = recv.next().await { 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() { @@ -737,7 +740,7 @@ impl Render for ProjectDiff { } else { None }; - let keybinding_focus_handle = self.focus_handle(cx); + let keybinding_focus_handle = self.focus_handle(cx).clone(); el.child( v_flex() .gap_1() @@ -1070,7 +1073,8 @@ 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 { - matches!(self.current_branch, Some(Branch { + match self.current_branch { + Some(Branch { upstream: Some(Upstream { tracking: @@ -1080,7 +1084,9 @@ impl RenderOnce for ProjectDiffEmptyState { .. }), .. - }) if (ahead > 0) == (ahead_by > 0) && (behind > 0) == (behind_by > 0)) + }) if (ahead > 0) == (ahead_by > 0) && (behind > 0) == (behind_by > 0) => true, + _ => false, + } }; let change_count = |current_branch: &Branch| -> (usize, usize) { @@ -1167,7 +1173,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)) }) } }), @@ -1326,14 +1332,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(); diff --git a/crates/git_ui/src/remote_output.rs b/crates/git_ui/src/remote_output.rs index 8437bf0d0d..03fbf4f917 100644 --- a/crates/git_ui/src/remote_output.rs +++ b/crates/git_ui/src/remote_output.rs @@ -24,7 +24,7 @@ impl RemoteAction { pub enum SuccessStyle { Toast, ToastWithLog { output: RemoteCommandOutput }, - PushPrLink { text: String, link: String }, + PushPrLink { link: String }, } pub struct SuccessMessage { @@ -37,7 +37,7 @@ pub fn format_output(action: &RemoteAction, output: RemoteCommandOutput) -> Succ RemoteAction::Fetch(remote) => { if output.stderr.is_empty() { SuccessMessage { - message: "Fetch: Already up to date".into(), + message: "Already up to date".into(), style: SuccessStyle::Toast, } } else { @@ -68,9 +68,10 @@ pub fn format_output(action: &RemoteAction, output: RemoteCommandOutput) -> Succ Ok(files_changed) }; - if output.stdout.ends_with("Already up to date.\n") { + + if output.stderr.starts_with("Everything up to date") { SuccessMessage { - message: "Pull: Already up to date".into(), + message: output.stderr.trim().to_owned(), style: SuccessStyle::Toast, } } else if output.stdout.starts_with("Updating") { @@ -118,42 +119,48 @@ pub fn format_output(action: &RemoteAction, output: RemoteCommandOutput) -> Succ } } RemoteAction::Push(branch_name, remote_ref) => { - let message = if output.stderr.ends_with("Everything up-to-date\n") { - "Push: Everything is up-to-date".to_string() - } else { - format!("Pushed {} to {}", branch_name, remote_ref.name) - }; - - let style = if output.stderr.ends_with("Everything up-to-date\n") { - Some(SuccessStyle::Toast) - } else if output.stderr.contains("\nremote: ") { + if output.stderr.contains("* [new branch]") { let pr_hints = [ - ("Create a pull request", "Create Pull Request"), // GitHub - ("Create pull request", "Create Pull Request"), // Bitbucket - ("create a merge request", "Create Merge Request"), // GitLab - ("View merge request", "View Merge Request"), // GitLab + // GitHub + "Create a pull request", + // Bitbucket + "Create pull request", + // GitLab + "create a merge request", ]; - pr_hints + let style = if pr_hints .iter() - .find(|(indicator, _)| output.stderr.contains(indicator)) - .and_then(|(_, mapped)| { - let finder = LinkFinder::new(); - finder - .links(&output.stderr) - .filter(|link| *link.kind() == LinkKind::Url) - .map(|link| link.start()..link.end()) - .next() - .map(|link| SuccessStyle::PushPrLink { - text: mapped.to_string(), - link: output.stderr[link].to_string(), - }) - }) + .any(|indicator| output.stderr.contains(indicator)) + { + let finder = LinkFinder::new(); + let first_link = finder + .links(&output.stderr) + .filter(|link| *link.kind() == LinkKind::Url) + .map(|link| link.start()..link.end()) + .next(); + if let Some(link) = first_link { + let link = output.stderr[link].to_string(); + SuccessStyle::PushPrLink { link } + } else { + SuccessStyle::ToastWithLog { output } + } + } else { + SuccessStyle::ToastWithLog { output } + }; + SuccessMessage { + message: format!("Published {} to {}", branch_name, remote_ref.name), + style, + } + } else if output.stderr.starts_with("Everything up to date") { + SuccessMessage { + message: output.stderr.trim().to_owned(), + style: SuccessStyle::Toast, + } } else { - None - }; - SuccessMessage { - message, - style: style.unwrap_or(SuccessStyle::ToastWithLog { output }), + SuccessMessage { + message: format!("Pushed {} to {}", branch_name, remote_ref.name), + style: SuccessStyle::ToastWithLog { output }, + } } } } @@ -162,7 +169,6 @@ pub fn format_output(action: &RemoteAction, output: RemoteCommandOutput) -> Succ #[cfg(test)] mod tests { use super::*; - use indoc::indoc; #[test] fn test_push_new_branch_pull_request() { @@ -175,7 +181,8 @@ mod tests { let output = RemoteCommandOutput { stdout: String::new(), - stderr: indoc! { " + stderr: String::from( + " Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0) remote: remote: Create a pull request for 'test' on GitHub by visiting: @@ -183,14 +190,13 @@ mod tests { remote: To example.com:test/test.git * [new branch] test -> test - "} - .to_string(), + ", + ), }; let msg = format_output(&action, output); - if let SuccessStyle::PushPrLink { text: hint, link } = &msg.style { - assert_eq!(hint, "Create Pull Request"); + if let SuccessStyle::PushPrLink { link } = &msg.style { assert_eq!(link, "https://example.com/test/test/pull/new/test"); } else { panic!("Expected PushPrLink variant"); @@ -208,7 +214,7 @@ mod tests { let output = RemoteCommandOutput { stdout: String::new(), - stderr: indoc! {" + stderr: String::from(" Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0) remote: remote: To create a merge request for test, visit: @@ -216,51 +222,16 @@ mod tests { remote: To example.com:test/test.git * [new branch] test -> test - "} - .to_string() - }; - - let msg = format_output(&action, output); - - if let SuccessStyle::PushPrLink { text, link } = &msg.style { - assert_eq!(text, "Create Merge Request"); - assert_eq!( - link, - "https://example.com/test/test/-/merge_requests/new?merge_request%5Bsource_branch%5D=test" - ); - } else { - panic!("Expected PushPrLink variant"); - } - } - - #[test] - fn test_push_branch_existing_merge_request() { - let action = RemoteAction::Push( - SharedString::new("test_branch"), - Remote { - name: SharedString::new("test_remote"), - }, - ); - - let output = RemoteCommandOutput { - stdout: String::new(), - stderr: indoc! {" - Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0) - remote: - remote: View merge request for test: - remote: https://example.com/test/test/-/merge_requests/99999 - remote: - To example.com:test/test.git - + 80bd3c83be...e03d499d2e test -> test - "} - .to_string(), + "), }; let msg = format_output(&action, output); - if let SuccessStyle::PushPrLink { text, link } = &msg.style { - assert_eq!(text, "View Merge Request"); - assert_eq!(link, "https://example.com/test/test/-/merge_requests/99999"); + if let SuccessStyle::PushPrLink { link } = &msg.style { + assert_eq!( + link, + "https://example.com/test/test/-/merge_requests/new?merge_request%5Bsource_branch%5D=test" + ); } else { panic!("Expected PushPrLink variant"); } @@ -277,12 +248,12 @@ mod tests { let output = RemoteCommandOutput { stdout: String::new(), - stderr: indoc! { " + stderr: String::from( + " To http://example.com/test/test.git * [new branch] test -> test ", - } - .to_string(), + ), }; let msg = format_output(&action, output); @@ -290,7 +261,10 @@ mod tests { if let SuccessStyle::ToastWithLog { output } = &msg.style { assert_eq!( output.stderr, - "To http://example.com/test/test.git\n * [new branch] test -> test\n" + " + To http://example.com/test/test.git + * [new branch] test -> test + " ); } else { panic!("Expected ToastWithLog variant"); diff --git a/crates/git_ui/src/repository_selector.rs b/crates/git_ui/src/repository_selector.rs index db080ab0b4..b5865e9a85 100644 --- a/crates/git_ui/src/repository_selector.rs +++ b/crates/git_ui/src/repository_selector.rs @@ -109,10 +109,7 @@ impl Focusable for RepositorySelector { impl Render for RepositorySelector { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div() - .key_context("GitRepositorySelector") - .w(self.width) - .child(self.picker.clone()) + div().w(self.width).child(self.picker.clone()) } } diff --git a/crates/git_ui/src/text_diff_view.rs b/crates/git_ui/src/text_diff_view.rs index ebf32d1b99..005c1e18b4 100644 --- a/crates/git_ui/src/text_diff_view.rs +++ b/crates/git_ui/src/text_diff_view.rs @@ -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()?; + let source_buffer = multibuffer.as_singleton()?.clone(); let selections = editor.selections.all::(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 buffer_changes_rx.recv().await.is_ok() { + while let Ok(_) = buffer_changes_rx.recv().await { 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(); + let base_text = base_buffer_snapshot.text().to_string(); 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, cx) = + let (workspace, mut 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()), - cx, + &mut cx, expected_diff, ); diff --git a/crates/go_to_line/src/cursor_position.rs b/crates/go_to_line/src/cursor_position.rs index e60a3651aa..322a791b13 100644 --- a/crates/go_to_line/src/cursor_position.rs +++ b/crates/go_to_line/src/cursor_position.rs @@ -1,4 +1,4 @@ -use editor::{Editor, EditorSettings, MultiBufferSnapshot}; +use editor::{Editor, MultiBufferSnapshot}; use gpui::{App, Entity, FocusHandle, Focusable, Subscription, Task, WeakEntity}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -95,8 +95,10 @@ impl CursorPosition { .ok() .unwrap_or(true); - if !is_singleton && let Some(debounce) = debounce { - cx.background_executor().timer(debounce).await; + if !is_singleton { + if let Some(debounce) = debounce { + cx.background_executor().timer(debounce).await; + } } editor @@ -106,7 +108,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; @@ -129,7 +131,7 @@ impl CursorPosition { cursor_position.selected_count.lines += 1; } } - if last_selection.as_ref().is_none_or(|last_selection| { + if last_selection.as_ref().map_or(true, |last_selection| { selection.id > last_selection.id }) { last_selection = Some(selection); @@ -207,13 +209,6 @@ impl CursorPosition { impl Render for CursorPosition { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> 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}{}", @@ -232,11 +227,13 @@ impl Render for CursorPosition { if let Some(editor) = workspace .active_item(cx) .and_then(|item| item.act_as::(cx)) - && let Some((_, buffer, _)) = editor.read(cx).active_excerpt(cx) { - workspace.toggle_modal(window, cx, |window, cx| { - crate::GoToLine::new(editor, buffer, window, 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) + }) + } } }); } @@ -311,14 +308,10 @@ impl Settings for LineIndicatorFormat { type FileContent = Option; fn load(sources: SettingsSources, _: &mut App) -> anyhow::Result { - let format = [ - sources.release_channel, - sources.operating_system, - sources.user, - ] - .into_iter() - .find_map(|value| value.copied().flatten()) - .unwrap_or(sources.default.ok_or_else(Self::missing_default)?); + let format = [sources.release_channel, sources.user] + .into_iter() + .find_map(|value| value.copied().flatten()) + .unwrap_or(sources.default.ok_or_else(Self::missing_default)?); Ok(format.0) } diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index 2afc72e989..1ac933e316 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -103,11 +103,11 @@ impl GoToLine { return; }; editor.update(cx, |editor, cx| { - if let Some(placeholder_text) = editor.placeholder_text() - && 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() { + if editor.text(cx).is_empty() { + let placeholder_text = placeholder_text.to_string(); + editor.set_text(placeholder_text, window, cx); + } } }); } @@ -157,7 +157,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), _ => {} } } @@ -712,7 +712,7 @@ mod tests { ) -> Entity { cx.dispatch_action(editor::actions::ToggleGoToLine); workspace.update(cx, |workspace, cx| { - workspace.active_modal::(cx).unwrap() + workspace.active_modal::(cx).unwrap().clone() }) } diff --git a/crates/google_ai/src/google_ai.rs b/crates/google_ai/src/google_ai.rs index ca0aa309b1..dfa51d024c 100644 --- a/crates/google_ai/src/google_ai.rs +++ b/crates/google_ai/src/google_ai.rs @@ -106,9 +106,10 @@ pub fn validate_generate_content_request(request: &GenerateContentRequest) -> Re .contents .iter() .find(|content| content.role == Role::User) - && user_content.parts.is_empty() { - bail!("User content must contain at least one part"); + if user_content.parts.is_empty() { + bail!("User content must contain at least one part"); + } } Ok(()) @@ -266,7 +267,7 @@ pub struct CitationMetadata { pub struct PromptFeedback { #[serde(skip_serializing_if = "Option::is_none")] pub block_reason: Option, - pub safety_ratings: Option>, + pub safety_ratings: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub block_reason_message: Option, } @@ -477,10 +478,10 @@ impl<'de> Deserialize<'de> for ModelName { model_id: id.to_string(), }) } else { - Err(serde::de::Error::custom(format!( + return Err(serde::de::Error::custom(format!( "Expected model name to begin with {}, got: {}", MODEL_NAME_PREFIX, string - ))) + ))); } } } diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 9f5b66087d..2bf49fa7d8 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -119,10 +119,9 @@ 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" +taffy = "=0.8.3" thiserror.workspace = true util.workspace = true uuid.workspace = true @@ -210,7 +209,7 @@ xkbcommon = { version = "0.8.0", features = [ "wayland", "x11", ], optional = true } -xim = { git = "https://github.com/zed-industries/xim-rs", rev = "c0a70c1bd2ce197364216e5e818a2cb3adb99a8d" , features = [ +xim = { git = "https://github.com/XDeme1/xim-rs", rev = "d50d461764c2213655cd9cf65a0ea94c70d3c4fd", features = [ "x11rb-xcb", "x11rb-client", ], optional = true } @@ -306,7 +305,3 @@ path = "examples/uniform_list.rs" [[example]] name = "window_shadow" path = "examples/window_shadow.rs" - -[[example]] -name = "grid_layout" -path = "examples/grid_layout.rs" diff --git a/crates/gpui/build.rs b/crates/gpui/build.rs index 0040046f90..2b574ebdd8 100644 --- a/crates/gpui/build.rs +++ b/crates/gpui/build.rs @@ -310,27 +310,15 @@ mod windows { &rust_binding_path, ); } - - { - let shader_path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()) - .join("src/platform/windows/color_text_raster.hlsl"); - compile_shader_for_module( - "emoji_rasterization", - &out_dir, - &fxc_path, - shader_path.to_str().unwrap(), - &rust_binding_path, - ); - } } /// 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") - && Path::new(&path).exists() - { - return path; + if let Ok(path) = std::env::var("GPUI_FXC_PATH") { + if Path::new(&path).exists() { + return path; + } } // Try to find in PATH @@ -338,10 +326,11 @@ mod windows { if let Ok(output) = std::process::Command::new("where.exe") .arg("fxc.exe") .output() - && output.status.success() { - let path = String::from_utf8_lossy(&output.stdout); - return path.trim().to_string(); + if output.status.success() { + let path = String::from_utf8_lossy(&output.stdout); + return path.trim().to_string(); + } } // Check the default path @@ -373,7 +362,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); @@ -386,7 +375,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( diff --git a/crates/gpui/examples/grid_layout.rs b/crates/gpui/examples/grid_layout.rs deleted file mode 100644 index f285497578..0000000000 --- a/crates/gpui/examples/grid_layout.rs +++ /dev/null @@ -1,80 +0,0 @@ -use gpui::{ - App, Application, Bounds, Context, Hsla, Window, WindowBounds, WindowOptions, div, prelude::*, - px, rgb, size, -}; - -// https://en.wikipedia.org/wiki/Holy_grail_(web_design) -struct HolyGrailExample {} - -impl Render for HolyGrailExample { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - let block = |color: Hsla| { - div() - .size_full() - .bg(color) - .border_1() - .border_dashed() - .rounded_md() - .border_color(gpui::white()) - .items_center() - }; - - div() - .gap_1() - .grid() - .bg(rgb(0x505050)) - .size(px(500.0)) - .shadow_lg() - .border_1() - .size_full() - .grid_cols(5) - .grid_rows(5) - .child( - block(gpui::white()) - .row_span(1) - .col_span_full() - .child("Header"), - ) - .child( - block(gpui::red()) - .col_span(1) - .h_56() - .child("Table of contents"), - ) - .child( - block(gpui::green()) - .col_span(3) - .row_span(3) - .child("Content"), - ) - .child( - block(gpui::blue()) - .col_span(1) - .row_span(3) - .child("AD :(") - .text_color(gpui::white()), - ) - .child( - block(gpui::black()) - .row_span(1) - .col_span_full() - .text_color(gpui::white()) - .child("Footer"), - ) - } -} - -fn main() { - Application::new().run(|cx: &mut App| { - let bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |_, cx| cx.new(|_| HolyGrailExample {}), - ) - .unwrap(); - cx.activate(true); - }); -} diff --git a/crates/gpui/examples/input.rs b/crates/gpui/examples/input.rs index 37115feaa5..52a5b08b96 100644 --- a/crates/gpui/examples/input.rs +++ b/crates/gpui/examples/input.rs @@ -137,14 +137,14 @@ impl TextInput { fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context) { 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) { 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, style.color) + (content.clone(), style.color) }; let run = TextRun { @@ -474,7 +474,7 @@ impl Element for TextElement { }, TextRun { len: display_text.len() - marked_range.end, - ..run + ..run.clone() }, ] .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) - && let Some(cursor) = prepaint.cursor.take() - { - window.paint_quad(cursor); + if focus_handle.is_focused(window) { + if let Some(cursor) = prepaint.cursor.take() { + window.paint_quad(cursor); + } } self.input.update(cx, |input, _cx| { @@ -595,7 +595,9 @@ impl Render for TextInput { .w_full() .p(px(4.)) .bg(white()) - .child(TextElement { input: cx.entity() }), + .child(TextElement { + input: cx.entity().clone(), + }), ) } } diff --git a/crates/gpui/examples/set_menus.rs b/crates/gpui/examples/set_menus.rs index 8a97a8d8a2..f53fff7c7f 100644 --- a/crates/gpui/examples/set_menus.rs +++ b/crates/gpui/examples/set_menus.rs @@ -1,6 +1,5 @@ use gpui::{ - App, Application, Context, Menu, MenuItem, SystemMenuType, Window, WindowOptions, actions, div, - prelude::*, rgb, + App, Application, Context, Menu, MenuItem, Window, WindowOptions, actions, div, prelude::*, rgb, }; struct SetMenus; @@ -28,11 +27,7 @@ fn main() { // Add menu items cx.set_menus(vec![Menu { name: "set_menus".into(), - items: vec![ - MenuItem::os_submenu("Services", SystemMenuType::Services), - MenuItem::separator(), - MenuItem::action("Quit", Quit), - ], + items: vec![MenuItem::action("Quit", Quit)], }]); cx.open_window(WindowOptions::default(), |_, cx| cx.new(|_| SetMenus {})) .unwrap(); diff --git a/crates/gpui/examples/tab_stop.rs b/crates/gpui/examples/tab_stop.rs index 8dbcbeccb7..1f6500f3e6 100644 --- a/crates/gpui/examples/tab_stop.rs +++ b/crates/gpui/examples/tab_stop.rs @@ -111,24 +111,8 @@ impl Render for Example { .flex_row() .gap_3() .items_center() - .child( - button("el1") - .tab_index(4) - .child("Button 1") - .on_click(cx.listener(|this, _, _, cx| { - this.message = "You have clicked Button 1.".into(); - cx.notify(); - })), - ) - .child( - button("el2") - .tab_index(5) - .child("Button 2") - .on_click(cx.listener(|this, _, _, cx| { - this.message = "You have clicked Button 2.".into(); - cx.notify(); - })), - ), + .child(button("el1").tab_index(4).child("Button 1")) + .child(button("el2").tab_index(5).child("Button 2")), ) } } diff --git a/crates/gpui/examples/text.rs b/crates/gpui/examples/text.rs index 66e9cff0aa..19214aebde 100644 --- a/crates/gpui/examples/text.rs +++ b/crates/gpui/examples/text.rs @@ -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) + .child(self.string.clone()) } } @@ -198,7 +198,7 @@ impl RenderOnce for CharacterGrid { "χ", "ψ", "∂", "а", "в", "Ж", "ж", "З", "з", "К", "к", "л", "м", "Н", "н", "Р", "р", "У", "у", "ф", "ч", "ь", "ы", "Э", "э", "Я", "я", "ij", "öẋ", ".,", "⣝⣑", "~", "*", "_", "^", "`", "'", "(", "{", "«", "#", "&", "@", "$", "¢", "%", "|", "?", "¶", "µ", - "❮", "<=", "!=", "==", "--", "++", "=>", "->", "🏀", "🎊", "😍", "❤️", "👍", "👎", + "❮", "<=", "!=", "==", "--", "++", "=>", "->", ]; let columns = 11; diff --git a/crates/gpui/examples/window_shadow.rs b/crates/gpui/examples/window_shadow.rs index 469017da79..06dde91133 100644 --- a/crates/gpui/examples/window_shadow.rs +++ b/crates/gpui/examples/window_shadow.rs @@ -165,8 +165,8 @@ impl Render for WindowShadow { }, ) .on_click(|e, window, _| { - if e.is_right_click() { - window.show_window_menu(e.position()); + if e.down.button == MouseButton::Right { + window.show_window_menu(e.up.position); } }) .text_color(black()) diff --git a/crates/gpui/src/action.rs b/crates/gpui/src/action.rs index 0b824fec34..b179076cd5 100644 --- a/crates/gpui/src/action.rs +++ b/crates/gpui/src/action.rs @@ -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 /// diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index b59d7e717a..ded7bae316 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -37,10 +37,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, PlatformKeyboardMapper, Point, PromptBuilder, - PromptButton, PromptHandle, PromptLevel, Render, RenderImage, RenderablePromptHandle, - Reservation, ScreenCaptureSource, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, - Window, WindowAppearance, WindowHandle, WindowId, WindowInvalidator, + PlatformDisplay, PlatformKeyboardLayout, Point, PromptBuilder, PromptButton, PromptHandle, + PromptLevel, Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource, + SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, Window, WindowAppearance, + WindowHandle, WindowId, WindowInvalidator, colors::{Colors, GlobalColors}, current_platform, hash, init_app_menus, }; @@ -263,7 +263,6 @@ pub struct App { pub(crate) focus_handles: Arc, pub(crate) keymap: Rc>, pub(crate) keyboard_layout: Box, - pub(crate) keyboard_mapper: Rc, pub(crate) global_action_listeners: FxHashMap>>, pending_effects: VecDeque, @@ -278,8 +277,6 @@ pub struct App { pub(crate) release_listeners: SubscriberSet, pub(crate) global_observers: SubscriberSet, pub(crate) quit_observers: SubscriberSet<(), QuitHandler>, - pub(crate) restart_observers: SubscriberSet<(), Handler>, - pub(crate) restart_path: Option, pub(crate) window_closed_observers: SubscriberSet<(), WindowClosedHandler>, pub(crate) layout_id_buffer: Vec, // We recycle this memory across layout requests. pub(crate) propagate_event: bool, @@ -313,7 +310,6 @@ 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 { @@ -339,7 +335,6 @@ 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(), @@ -354,8 +349,6 @@ impl App { keyboard_layout_observers: SubscriberSet::new(), global_observers: SubscriberSet::new(), quit_observers: SubscriberSet::new(), - restart_observers: SubscriberSet::new(), - restart_path: None, window_closed_observers: SubscriberSet::new(), layout_id_buffer: Default::default(), propagate_event: true, @@ -371,7 +364,7 @@ impl App { }), }); - init_app_menus(platform.as_ref(), &app.borrow()); + init_app_menus(platform.as_ref(), &mut app.borrow_mut()); platform.on_keyboard_layout_change(Box::new({ let app = Rc::downgrade(&app); @@ -379,7 +372,6 @@ 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)); @@ -428,11 +420,6 @@ impl App { self.keyboard_layout.as_ref() } - /// Get the current keyboard mapper. - pub fn keyboard_mapper(&self) -> &Rc { - &self.keyboard_mapper - } - /// Invokes a handler when the current keyboard layout changes pub fn on_keyboard_layout_change(&self, mut callback: F) -> Subscription where @@ -825,9 +812,8 @@ impl App { pub fn prompt_for_new_path( &self, directory: &Path, - suggested_name: Option<&str>, ) -> oneshot::Receiver>> { - self.platform.prompt_for_new_path(directory, suggested_name) + self.platform.prompt_for_new_path(directory) } /// Reveals the specified path at the platform level, such as in Finder on macOS. @@ -846,16 +832,8 @@ impl App { } /// Restarts the application. - pub fn restart(&mut self) { - self.restart_observers - .clone() - .retain(&(), |observer| observer(self)); - self.platform.restart(self.restart_path.take()) - } - - /// Sets the path to use when restarting the application. - pub fn set_restart_path(&mut self, path: PathBuf) { - self.restart_path = Some(path); + pub fn restart(&self, binary_path: Option) { + self.platform.restart(binary_path) } /// Returns the HTTP client for the application. @@ -1319,7 +1297,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)); }) } @@ -1341,7 +1319,7 @@ impl App { } inner( - &self.keystroke_observers, + &mut self.keystroke_observers, Box::new(move |event, window, cx| { f(event, window, cx); true @@ -1367,7 +1345,7 @@ impl App { } inner( - &self.keystroke_interceptors, + &mut self.keystroke_interceptors, Box::new(move |event, window, cx| { f(event, window, cx); true @@ -1488,21 +1466,6 @@ impl App { subscription } - /// Register a callback to be invoked when the application is about to restart. - /// - /// These callbacks are called before any `on_app_quit` callbacks. - pub fn on_app_restart(&self, mut on_restart: impl 'static + FnMut(&mut App)) -> Subscription { - let (subscription, activate) = self.restart_observers.insert( - (), - Box::new(move |cx| { - on_restart(cx); - true - }), - ); - activate(); - subscription - } - /// Register a callback to be invoked when a window is closed /// The window is no longer accessible at the point this callback is invoked. pub fn on_window_closed(&self, mut on_closed: impl FnMut(&mut App) + 'static) -> Subscription { @@ -1525,11 +1488,12 @@ 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() - && let Ok(window_action_available) = + if let Some(window) = self.active_window() { + if 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 @@ -1614,26 +1578,27 @@ impl App { .insert(action.as_any().type_id(), global_listeners); } - if self.propagate_event - && let Some(mut global_listeners) = self + if self.propagate_event { + if 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( + global_listeners.extend( + self.global_action_listeners + .remove(&action.as_any().type_id()) + .unwrap_or_default(), + ); + self.global_action_listeners - .remove(&action.as_any().type_id()) - .unwrap_or_default(), - ); - - self.global_action_listeners - .insert(action.as_any().type_id(), global_listeners); + .insert(action.as_any().type_id(), global_listeners); + } } } @@ -1716,8 +1681,8 @@ impl App { .unwrap_or_else(|| { is_first = true; let future = A::load(source.clone(), self); - - self.background_executor().spawn(future).shared() + let task = self.background_executor().spawn(future).shared(); + task }); self.loading_assets.insert(asset_id, Box::new(task.clone())); @@ -1924,7 +1889,7 @@ impl AppContext for App { G: Global, { let mut g = self.global::(); - callback(g, self) + callback(&g, self) } } diff --git a/crates/gpui/src/app/async_context.rs b/crates/gpui/src/app/async_context.rs index 5eb4362904..d9d21c0244 100644 --- a/crates/gpui/src/app/async_context.rs +++ b/crates/gpui/src/app/async_context.rs @@ -465,7 +465,7 @@ impl VisualContext for AsyncWindowContext { V: Focusable, { self.window.update(self, |_, window, cx| { - view.read(cx).focus_handle(cx).focus(window); + view.read(cx).focus_handle(cx).clone().focus(window); }) } } diff --git a/crates/gpui/src/app/context.rs b/crates/gpui/src/app/context.rs index 1112878a66..392be2ffe9 100644 --- a/crates/gpui/src/app/context.rs +++ b/crates/gpui/src/app/context.rs @@ -164,20 +164,6 @@ impl<'a, T: 'static> Context<'a, T> { subscription } - /// Register a callback to be invoked when the application is about to restart. - pub fn on_app_restart( - &self, - mut on_restart: impl FnMut(&mut T, &mut App) + 'static, - ) -> Subscription - where - T: 'static, - { - let handle = self.weak_entity(); - self.app.on_app_restart(move |cx| { - handle.update(cx, |entity, cx| on_restart(entity, cx)).ok(); - }) - } - /// Arrange for the given function to be invoked whenever the application is quit. /// The future returned from this callback will be polled for up to [crate::SHUTDOWN_TIMEOUT] until the app fully quits. pub fn on_app_quit( @@ -189,15 +175,20 @@ impl<'a, T: 'static> Context<'a, T> { T: 'static, { let handle = self.weak_entity(); - self.app.on_app_quit(move |cx| { - let future = handle.update(cx, |entity, cx| on_quit(entity, cx)).ok(); - async move { - if let Some(future) = future { - future.await; + let (subscription, activate) = self.app.quit_observers.insert( + (), + Box::new(move |cx| { + let future = handle.update(cx, |entity, cx| on_quit(entity, cx)).ok(); + async move { + if let Some(future) = future { + future.await; + } } - } - .boxed_local() - }) + .boxed_local() + }), + ); + activate(); + subscription } /// Tell GPUI that this entity has changed and observers of it should be notified. @@ -472,7 +463,7 @@ impl<'a, T: 'static> Context<'a, T> { let view = self.weak_entity(); inner( - &self.keystroke_observers, + &mut 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 +601,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() - && 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() { + 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) + } } }) .is_ok() diff --git a/crates/gpui/src/app/entity_map.rs b/crates/gpui/src/app/entity_map.rs index ea52b46d9f..fccb417caa 100644 --- a/crates/gpui/src/app/entity_map.rs +++ b/crates/gpui/src/app/entity_map.rs @@ -231,15 +231,14 @@ 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, } } @@ -662,7 +661,7 @@ pub struct WeakEntity { impl std::fmt::Debug for WeakEntity { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct(type_name::()) + f.debug_struct(&type_name::()) .field("entity_id", &self.any_entity.entity_id) .field("entity_type", &type_name::()) .finish() @@ -787,7 +786,7 @@ impl PartialOrd for WeakEntity { #[cfg(any(test, feature = "leak-detection"))] static LEAK_BACKTRACE: std::sync::LazyLock = - std::sync::LazyLock::new(|| std::env::var("LEAK_BACKTRACE").is_ok_and(|b| !b.is_empty())); + std::sync::LazyLock::new(|| std::env::var("LEAK_BACKTRACE").map_or(false, |b| !b.is_empty())); #[cfg(any(test, feature = "leak-detection"))] #[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)] diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index c65c045f6b..35e6032671 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -134,7 +134,7 @@ impl TestAppContext { app: App::new_app(platform.clone(), asset_source, http_client), background_executor, foreground_executor, - dispatcher, + dispatcher: dispatcher.clone(), test_platform: platform, text_system, fn_name, @@ -192,7 +192,6 @@ impl TestAppContext { &self.foreground_executor } - #[expect(clippy::wrong_self_convention)] fn new(&mut self, build_entity: impl FnOnce(&mut Context) -> T) -> Entity { let mut cx = self.app.borrow_mut(); cx.new(build_entity) @@ -220,7 +219,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, &cx); + let bounds = Bounds::maximized(None, &mut cx); cx.open_window( WindowOptions { window_bounds: Some(WindowBounds::Windowed(bounds)), @@ -234,7 +233,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, &cx); + let bounds = Bounds::maximized(None, &mut cx); let window = cx .open_window( WindowOptions { @@ -245,7 +244,7 @@ impl TestAppContext { ) .unwrap(); drop(cx); - let cx = VisualTestContext::from_window(*window.deref(), self).into_mut(); + let cx = VisualTestContext::from_window(*window.deref(), self).as_mut(); cx.run_until_parked(); cx } @@ -262,7 +261,7 @@ impl TestAppContext { V: 'static + Render, { let mut cx = self.app.borrow_mut(); - let bounds = Bounds::maximized(None, &cx); + let bounds = Bounds::maximized(None, &mut cx); let window = cx .open_window( WindowOptions { @@ -274,7 +273,7 @@ impl TestAppContext { .unwrap(); drop(cx); let view = window.root(self).unwrap(); - let cx = VisualTestContext::from_window(*window.deref(), self).into_mut(); + let cx = VisualTestContext::from_window(*window.deref(), self).as_mut(); cx.run_until_parked(); // it might be nice to try and cleanup these at the end of each test. @@ -339,7 +338,7 @@ impl TestAppContext { /// Returns all windows open in the test. pub fn windows(&self) -> Vec { - self.app.borrow().windows() + self.app.borrow().windows().clone() } /// Run the given task on the main thread. @@ -586,7 +585,7 @@ impl Entity { cx.executor().advance_clock(advance_clock_by); async move { - let notification = crate::util::smol_timeout(duration, rx.recv()) + let notification = crate::util::timeout(duration, rx.recv()) .await .expect("next notification timed out"); drop(subscription); @@ -619,7 +618,7 @@ impl Entity { } }), cx.subscribe(self, { - let mut tx = tx; + let mut tx = tx.clone(); move |_, _: &Evt, _| { tx.blocking_send(()).ok(); } @@ -630,7 +629,7 @@ impl Entity { let handle = self.downgrade(); async move { - crate::util::smol_timeout(Duration::from_secs(1), async move { + crate::util::timeout(Duration::from_secs(1), async move { loop { { let cx = cx.borrow(); @@ -883,7 +882,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 into_mut(self) -> &'static mut Self { + pub fn as_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. @@ -1026,7 +1025,7 @@ impl VisualContext for VisualTestContext { fn focus(&mut self, view: &Entity) -> Self::Result<()> { self.window .update(&mut self.cx, |_, window, cx| { - view.read(cx).focus_handle(cx).focus(window) + view.read(cx).focus_handle(cx).clone().focus(window) }) .unwrap() } diff --git a/crates/gpui/src/arena.rs b/crates/gpui/src/arena.rs index 0983bd2345..ee72d0e964 100644 --- a/crates/gpui/src/arena.rs +++ b/crates/gpui/src/arena.rs @@ -142,7 +142,7 @@ impl Arena { 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::trace!( + log::info!( "increased element arena capacity to {}kb", self.capacity() / 1024, ); diff --git a/crates/gpui/src/color.rs b/crates/gpui/src/color.rs index cb7329c03f..a16c8f46be 100644 --- a/crates/gpui/src/color.rs +++ b/crates/gpui/src/color.rs @@ -35,7 +35,6 @@ pub(crate) fn swap_rgba_pa_to_bgra(color: &mut [u8]) { /// An RGBA color #[derive(PartialEq, Clone, Copy, Default)] -#[repr(C)] pub struct Rgba { /// The red component of the color, in the range 0.0 to 1.0 pub r: f32, @@ -905,9 +904,9 @@ mod tests { assert_eq!(background.solid, color); assert_eq!(background.opacity(0.5).solid, color.opacity(0.5)); - assert!(!background.is_transparent()); + assert_eq!(background.is_transparent(), false); background.solid = hsla(0.0, 0.0, 0.0, 0.0); - assert!(background.is_transparent()); + assert_eq!(background.is_transparent(), true); } #[test] @@ -921,7 +920,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!(!background.is_transparent()); - assert!(background.opacity(0.0).is_transparent()); + assert_eq!(background.is_transparent(), false); + assert_eq!(background.opacity(0.0).is_transparent(), true); } } diff --git a/crates/gpui/src/element.rs b/crates/gpui/src/element.rs index f537bc5ac8..e5f49c7be1 100644 --- a/crates/gpui/src/element.rs +++ b/crates/gpui/src/element.rs @@ -603,8 +603,10 @@ impl AnyElement { self.0.prepaint(window, cx); - if !focus_assigned && let Some(focus_id) = window.next_frame.focus { - return FocusHandle::for_id(focus_id, &cx.focus_handles); + if !focus_assigned { + if let Some(focus_id) = window.next_frame.focus { + return FocusHandle::for_id(focus_id, &cx.focus_handles); + } } None diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index c9826b704e..fa47758581 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -19,15 +19,14 @@ use crate::{ Action, AnyDrag, AnyElement, AnyTooltip, AnyView, App, Bounds, ClickEvent, DispatchPhase, Element, ElementId, Entity, FocusHandle, Global, GlobalElementId, Hitbox, HitboxBehavior, HitboxId, InspectorElementId, IntoElement, IsZero, KeyContext, KeyDownEvent, KeyUpEvent, - KeyboardButton, KeyboardClickEvent, LayoutId, ModifiersChangedEvent, MouseButton, - MouseClickEvent, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Overflow, ParentElement, Pixels, - Point, Render, ScrollWheelEvent, SharedString, Size, Style, StyleRefinement, Styled, Task, - TooltipId, Visibility, Window, WindowControlArea, point, px, size, + LayoutId, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, + Overflow, ParentElement, Pixels, Point, Render, ScrollWheelEvent, SharedString, Size, Style, + StyleRefinement, Styled, Task, TooltipId, Visibility, Window, WindowControlArea, point, px, + size, }; use collections::HashMap; use refineable::Refineable; use smallvec::SmallVec; -use stacksafe::{StackSafe, stacksafe}; use std::{ any::{Any, TypeId}, cell::RefCell, @@ -286,20 +285,21 @@ impl Interactivity { { self.mouse_move_listeners .push(Box::new(move |event, phase, hitbox, window, cx| { - if phase == DispatchPhase::Capture - && let Some(drag) = &cx.active_drag - && drag.value.as_ref().type_id() == TypeId::of::() - { - (listener)( - &DragMoveEvent { - event: event.clone(), - bounds: hitbox.bounds, - drag: PhantomData, - dragged_item: Arc::clone(&drag.value), - }, - window, - cx, - ); + if phase == DispatchPhase::Capture { + if let Some(drag) = &cx.active_drag { + if drag.value.as_ref().type_id() == TypeId::of::() { + (listener)( + &DragMoveEvent { + event: event.clone(), + bounds: hitbox.bounds, + drag: PhantomData, + dragged_item: Arc::clone(&drag.value), + }, + window, + cx, + ); + } + } } })); } @@ -484,9 +484,10 @@ impl Interactivity { where Self: Sized, { - self.click_listeners.push(Rc::new(move |event, window, cx| { - listener(event, window, cx) - })); + self.click_listeners + .push(Box::new(move |event, window, cx| { + listener(event, window, cx) + })); } /// On drag initiation, this callback will be used to create a new view to render the dragged value for a @@ -1155,7 +1156,7 @@ pub(crate) type MouseMoveListener = pub(crate) type ScrollWheelListener = Box; -pub(crate) type ClickListener = Rc; +pub(crate) type ClickListener = Box; pub(crate) type DragListener = Box, &mut Window, &mut App) -> AnyView + 'static>; @@ -1195,7 +1196,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<[StackSafe; 2]>, + children: SmallVec<[AnyElement; 2]>, prepaint_listener: Option>, &mut Window, &mut App) + 'static>>, image_cache: Option>, } @@ -1256,8 +1257,7 @@ impl InteractiveElement for Div { impl ParentElement for Div { fn extend(&mut self, elements: impl IntoIterator) { - self.children - .extend(elements.into_iter().map(StackSafe::new)) + self.children.extend(elements) } } @@ -1273,7 +1273,6 @@ impl Element for Div { self.interactivity.source_location() } - #[stacksafe] fn request_layout( &mut self, global_id: Option<&GlobalElementId>, @@ -1309,7 +1308,6 @@ impl Element for Div { (layout_id, DivFrameState { child_layout_ids }) } - #[stacksafe] fn prepaint( &mut self, global_id: Option<&GlobalElementId>, @@ -1379,7 +1377,6 @@ impl Element for Div { ) } - #[stacksafe] fn paint( &mut self, global_id: Option<&GlobalElementId>, @@ -1513,14 +1510,15 @@ 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() - && 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() { + 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(); + } } } @@ -1528,35 +1526,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() - && 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 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); + if let Some(index) = self.tab_index { + handle = handle.tab_index(index).tab_stop(true); + } + + self.tracked_focus_handle = Some(handle); } - - 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)) - && let Some(element_state) = element_state.as_mut() + } else if self.base_style.overflow.x == Some(Overflow::Scroll) + || self.base_style.overflow.y == Some(Overflow::Scroll) { - self.scroll_offset = Some( - element_state - .scroll_offset - .get_or_insert_with(Rc::default) - .clone(), - ); + if let Some(element_state) = element_state.as_mut() { + 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); @@ -1952,12 +1950,6 @@ impl Interactivity { window: &mut Window, cx: &mut App, ) { - let is_focused = self - .tracked_focus_handle - .as_ref() - .map(|handle| handle.is_focused(window)) - .unwrap_or(false); - // If this element can be focused, register a mouse down listener // that will automatically transfer focus when hitting the element. // This behavior can be suppressed by using `cx.prevent_default()`. @@ -2029,27 +2021,26 @@ impl Interactivity { let hitbox = hitbox.clone(); window.on_mouse_event({ move |_: &MouseUpEvent, phase, 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"); + 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); - } + 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(); + } } } } @@ -2093,60 +2084,34 @@ impl Interactivity { } let mut pending_mouse_down = pending_mouse_down.borrow_mut(); - 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(); - } - } - }); - - if is_focused { - // Press enter, space to trigger click, when the element is focused. - window.on_key_event({ - let click_listeners = click_listeners.clone(); - let hitbox = hitbox.clone(); - move |event: &KeyUpEvent, phase, window, cx| { - if phase.bubble() && !window.default_prevented() { - let stroke = &event.keystroke; - let keyboard_button = if stroke.key.eq("enter") { - Some(KeyboardButton::Enter) - } else if stroke.key.eq("space") { - Some(KeyboardButton::Space) - } else { - None - }; - - if let Some(button) = keyboard_button - && !stroke.modifiers.modified() - { - let click_event = ClickEvent::Keyboard(KeyboardClickEvent { - button, - bounds: hitbox.bounds, + 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, }); - - for listener in &click_listeners { - listener(&click_event, window, cx); - } + pending_mouse_down.take(); + window.refresh(); + cx.stop_propagation(); } } } - }); - } + } + }); window.on_mouse_event({ let mut captured_mouse_down = None; @@ -2173,10 +2138,10 @@ impl Interactivity { // Fire click handlers during the bubble phase. DispatchPhase::Bubble => { if let Some(mouse_down) = captured_mouse_down.take() { - let mouse_click = ClickEvent::Mouse(MouseClickEvent { + let mouse_click = ClickEvent { down: mouse_down, up: event.clone(), - }); + }; for listener in &click_listeners { listener(&mouse_click, window, cx); } @@ -2274,7 +2239,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 - .is_some_and(|group_hitbox_id| group_hitbox_id.is_hovered(window)); + .map_or(false, |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 { @@ -2420,32 +2385,33 @@ 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() - && focus_handle.within_focused(window, cx) - { - style.refine(in_focus_style); + 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(focus_style) = self.focus_style.as_ref() - && focus_handle.is_focused(window) - { - style.refine(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(hitbox) = hitbox { if !cx.has_active_drag() { - if let Some(group_hover) = self.group_hover_style.as_ref() - && let Some(group_hitbox_id) = GroupHitboxes::get(&group_hover.group, cx) - && group_hitbox_id.is_hovered(window) - { - style.refine(&group_hover.style); + if let Some(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(hover_style) = self.hover_style.as_ref() - && hitbox.is_hovered(window) - { - style.refine(hover_style); + if let Some(hover_style) = self.hover_style.as_ref() { + if hitbox.is_hovered(window) { + style.refine(hover_style); + } } } @@ -2459,10 +2425,12 @@ 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) { - style.refine(&group_drag_style.style); + if *state_type == drag.value.as_ref().type_id() + && group_hitbox_id.is_hovered(window) + { + style.refine(&group_drag_style.style); + } } } @@ -2484,16 +2452,16 @@ impl Interactivity { .clicked_state .get_or_insert_with(Default::default) .borrow(); - if clicked_state.group - && let Some(group) = self.group_active_style.as_ref() - { - style.refine(&group.style) + if clicked_state.group { + if let Some(group) = self.group_active_style.as_ref() { + style.refine(&group.style) + } } - if let Some(active_style) = self.active_style.as_ref() - && clicked_state.element - { - style.refine(active_style) + if let Some(active_style) = self.active_style.as_ref() { + if clicked_state.element { + style.refine(active_style) + } } } @@ -2614,7 +2582,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.is_some_and(|tooltip_id| tooltip_id.is_hovered(window)) { + if !tooltip_id.map_or(false, |tooltip_id| tooltip_id.is_hovered(window)) { clear_active_tooltip_if_not_hoverable(&active_tooltip, window); } } @@ -2623,7 +2591,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.is_some_and(|tooltip_id| tooltip_id.is_hovered(window)) { + if !tooltip_id.map_or(false, |tooltip_id| tooltip_id.is_hovered(window)) { clear_active_tooltip_if_not_hoverable(&active_tooltip, window); } } @@ -2779,7 +2747,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(); diff --git a/crates/gpui/src/elements/image_cache.rs b/crates/gpui/src/elements/image_cache.rs index ee1436134a..e7bdeaf9eb 100644 --- a/crates/gpui/src/elements/image_cache.rs +++ b/crates/gpui/src/elements/image_cache.rs @@ -64,7 +64,7 @@ mod any_image_cache { cx: &mut App, ) -> Option, ImageCacheError>> { let image_cache = image_cache.clone().downcast::().unwrap(); - image_cache.update(cx, |image_cache, cx| image_cache.load(resource, window, cx)) + return 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) - && let Some(Ok(image)) = item.get() - { - cx.drop_image(image, Some(window)); + if let Some(mut item) = self.0.remove(&hash) { + if let Some(Ok(image)) = item.get() { + cx.drop_image(image, Some(window)); + } } } diff --git a/crates/gpui/src/elements/img.rs b/crates/gpui/src/elements/img.rs index 893860d7e1..993b319b69 100644 --- a/crates/gpui/src/elements/img.rs +++ b/crates/gpui/src/elements/img.rs @@ -379,12 +379,13 @@ 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 - && 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 { + 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); + } } } else { let current_view = window.current_view(); @@ -475,7 +476,7 @@ impl Element for Img { .paint_image( new_bounds, corner_radii, - data, + data.clone(), layout_state.frame_index, self.style.grayscale, ) diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index 6758f4eee1..328a6a4cc1 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -16,18 +16,12 @@ use crate::{ use collections::VecDeque; use refineable::Refineable as _; use std::{cell::RefCell, ops::Range, rc::Rc}; -use sum_tree::{Bias, Dimensions, SumTree}; - -type RenderItemFn = dyn FnMut(usize, &mut Window, &mut App) -> AnyElement + 'static; +use sum_tree::{Bias, SumTree}; /// Construct a new list element -pub fn list( - state: ListState, - render_item: impl FnMut(usize, &mut Window, &mut App) -> AnyElement + 'static, -) -> List { +pub fn list(state: ListState) -> List { List { state, - render_item: Box::new(render_item), style: StyleRefinement::default(), sizing_behavior: ListSizingBehavior::default(), } @@ -36,7 +30,6 @@ pub fn list( /// A list element pub struct List { state: ListState, - render_item: Box, style: StyleRefinement, sizing_behavior: ListSizingBehavior, } @@ -62,6 +55,7 @@ impl std::fmt::Debug for ListState { struct StateInner { last_layout_bounds: Option>, last_padding: Option>, + render_item: Box AnyElement>, items: SumTree, logical_scroll_top: Option, alignment: ListAlignment, @@ -192,10 +186,19 @@ impl ListState { /// above and below the visible area. Elements within this area will /// be measured even though they are not visible. This can help ensure /// that the list doesn't flicker or pop in when scrolling. - pub fn new(item_count: usize, alignment: ListAlignment, overdraw: Pixels) -> Self { + pub fn new( + item_count: usize, + alignment: ListAlignment, + overdraw: Pixels, + render_item: R, + ) -> Self + where + R: 'static + FnMut(usize, &mut Window, &mut App) -> AnyElement, + { let this = Self(Rc::new(RefCell::new(StateInner { last_layout_bounds: None, last_padding: None, + render_item: Box::new(render_item), items: SumTree::default(), logical_scroll_top: None, alignment, @@ -368,14 +371,14 @@ impl ListState { return None; } - let mut cursor = state.items.cursor::>(&()); + let mut cursor = state.items.cursor::<(Count, Height)>(&()); cursor.seek(&Count(scroll_top.item_ix), Bias::Right); let scroll_top = cursor.start().1.0 + scroll_top.offset_in_item; cursor.seek_forward(&Count(ix), Bias::Right); if let Some(&ListItem::Measured { size, .. }) = cursor.item() { - let &Dimensions(Count(count), Height(top), _) = cursor.start(); + let &(Count(count), Height(top)) = cursor.start(); if count == ix { let top = bounds.top() + top - scroll_top; return Some(Bounds::from_corners( @@ -529,7 +532,6 @@ impl StateInner { available_width: Option, available_height: Pixels, padding: &Edges, - render_item: &mut RenderItemFn, window: &mut Window, cx: &mut App, ) -> LayoutItemsResponse { @@ -564,7 +566,7 @@ impl StateInner { // If we're within the visible area or the height wasn't cached, render and measure the item's element if visible_height < available_height || size.is_none() { let item_index = scroll_top.item_ix + ix; - let mut element = render_item(item_index, window, cx); + let mut element = (self.render_item)(item_index, window, cx); let element_size = element.layout_as_root(available_item_space, window, cx); size = Some(element_size); if visible_height < available_height { @@ -599,7 +601,7 @@ impl StateInner { cursor.prev(); if let Some(item) = cursor.item() { let item_index = cursor.start().0; - let mut element = render_item(item_index, window, cx); + let mut element = (self.render_item)(item_index, window, cx); let element_size = element.layout_as_root(available_item_space, window, cx); let focus_handle = item.focus_handle(); rendered_height += element_size.height; @@ -648,7 +650,7 @@ impl StateInner { let size = if let ListItem::Measured { size, .. } = item { *size } else { - let mut element = render_item(cursor.start().0, window, cx); + let mut element = (self.render_item)(cursor.start().0, window, cx); element.layout_as_root(available_item_space, window, cx) }; @@ -681,7 +683,7 @@ impl StateInner { while let Some(item) = cursor.item() { if item.contains_focused(window, cx) { let item_index = cursor.start().0; - let mut element = render_item(cursor.start().0, window, cx); + let mut element = (self.render_item)(cursor.start().0, window, cx); let size = element.layout_as_root(available_item_space, window, cx); item_layouts.push_back(ItemLayout { index: item_index, @@ -706,7 +708,6 @@ impl StateInner { bounds: Bounds, padding: Edges, autoscroll: bool, - render_item: &mut RenderItemFn, window: &mut Window, cx: &mut App, ) -> Result { @@ -715,7 +716,6 @@ impl StateInner { Some(bounds.size.width), bounds.size.height, &padding, - render_item, window, cx, ); @@ -732,44 +732,47 @@ impl StateInner { item.element.prepaint_at(item_origin, window, cx); }); - 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::(&()); - 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) + 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, }); - height -= size.height; - } + } else if autoscroll_bounds.bottom() > bounds.bottom() { + let mut cursor = self.items.cursor::(&()); + cursor.seek(&Count(item.index), Bias::Right); + let mut height = bounds.size.height - padding.top - padding.bottom; - return Err(ListOffset { - item_ix: cursor.start().0, - offset_in_item: if height < Pixels::ZERO { - -height - } else { - Pixels::ZERO - }, - }); + // 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 = + (self.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 + }, + }); + } } } @@ -873,14 +876,8 @@ impl Element for List { window.rem_size(), ); - let layout_response = state.layout_items( - None, - available_height, - &padding, - &mut self.render_item, - window, - cx, - ); + let layout_response = + state.layout_items(None, available_height, &padding, window, cx); let max_element_width = layout_response.max_item_width; let summary = state.items.summary(); @@ -938,10 +935,9 @@ 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 - .is_none_or(|last_bounds| last_bounds.size.width != bounds.size.width) - { + if state.last_layout_bounds.map_or(true, |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(), @@ -955,16 +951,15 @@ impl Element for List { let padding = style .padding .to_pixels(bounds.size.into(), window.rem_size()); - let layout = - match state.prepaint_items(bounds, padding, true, &mut self.render_item, window, cx) { - Ok(layout) => layout, - Err(autoscroll_request) => { - state.logical_scroll_top = Some(autoscroll_request); - state - .prepaint_items(bounds, padding, false, &mut self.render_item, window, cx) - .unwrap() - } - }; + let layout = match state.prepaint_items(bounds, padding, true, window, cx) { + Ok(layout) => layout, + Err(autoscroll_request) => { + state.logical_scroll_top = Some(autoscroll_request); + state + .prepaint_items(bounds, padding, false, window, cx) + .unwrap() + } + }; state.last_layout_bounds = Some(bounds); state.last_padding = Some(padding); @@ -1113,7 +1108,9 @@ mod test { let cx = cx.add_empty_window(); - let state = ListState::new(5, crate::ListAlignment::Top, px(10.)); + let state = ListState::new(5, crate::ListAlignment::Top, px(10.), |_, _, _| { + div().h(px(10.)).w_full().into_any() + }); // Ensure that the list is scrolled to the top state.scroll_to(gpui::ListOffset { @@ -1124,11 +1121,7 @@ mod test { struct TestView(ListState); impl Render for TestView { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - list(self.0.clone(), |_, _, _| { - div().h(px(10.)).w_full().into_any() - }) - .w_full() - .h_full() + list(self.0.clone()).w_full().h_full() } } @@ -1161,16 +1154,14 @@ mod test { let cx = cx.add_empty_window(); - let state = ListState::new(5, crate::ListAlignment::Top, px(10.)); + let state = ListState::new(5, crate::ListAlignment::Top, px(10.), |_, _, _| { + div().h(px(20.)).w_full().into_any() + }); struct TestView(ListState); impl Render for TestView { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - list(self.0.clone(), |_, _, _| { - div().h(px(20.)).w_full().into_any() - }) - .w_full() - .h_full() + list(self.0.clone()).w_full().h_full() } } diff --git a/crates/gpui/src/elements/text.rs b/crates/gpui/src/elements/text.rs index b5e0712796..014f617e2c 100644 --- a/crates/gpui/src/elements/text.rs +++ b/crates/gpui/src/elements/text.rs @@ -326,7 +326,7 @@ impl TextLayout { vec![text_style.to_run(text.len())] }; - window.request_measured_layout(Default::default(), { + let layout_id = window.request_measured_layout(Default::default(), { let element_state = self.clone(); move |known_dimensions, available_space, window, cx| { @@ -356,11 +356,12 @@ impl TextLayout { (None, "".into()) }; - 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(); + 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(); + } } let mut line_wrapper = cx.text_system().line_wrapper(text_style.font(), font_size); @@ -416,7 +417,9 @@ impl TextLayout { size } - }) + }); + + layout_id } fn prepaint(&self, bounds: Bounds, text: &str) { @@ -760,13 +763,14 @@ 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) - && self + if let Ok(ix) = text_layout.index_for_position(mouse_position) { + if 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(); @@ -799,13 +803,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) - && let Ok(mouse_down_index) = + if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) { + if 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(); + } } }); } diff --git a/crates/gpui/src/geometry.rs b/crates/gpui/src/geometry.rs index 87cabc8cd9..74be6344f9 100644 --- a/crates/gpui/src/geometry.rs +++ b/crates/gpui/src/geometry.rs @@ -9,14 +9,12 @@ use refineable::Refineable; use schemars::{JsonSchema, json_schema}; use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; use std::borrow::Cow; -use std::ops::Range; use std::{ cmp::{self, PartialOrd}, fmt::{self, Display}, hash::Hash, ops::{Add, Div, Mul, MulAssign, Neg, Sub}, }; -use taffy::prelude::{TaffyGridLine, TaffyGridSpan}; use crate::{App, DisplayId}; @@ -1046,7 +1044,7 @@ where size: self.size.clone() + size( amount.left.clone() + amount.right.clone(), - amount.top.clone() + amount.bottom, + amount.top.clone() + amount.bottom.clone(), ), } } @@ -1159,10 +1157,10 @@ where /// Computes the space available within outer bounds. pub fn space_within(&self, outer: &Self) -> Edges { Edges { - top: self.top() - outer.top(), - right: outer.right() - self.right(), - bottom: outer.bottom() - self.bottom(), - left: self.left() - outer.left(), + 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(), } } } @@ -1641,7 +1639,7 @@ impl Bounds { } /// Convert the bounds from logical pixels to physical pixels - pub fn to_device_pixels(self, factor: f32) -> Bounds { + pub fn to_device_pixels(&self, factor: f32) -> Bounds { Bounds { origin: point( DevicePixels((self.origin.x.0 * factor).round() as i32), @@ -1712,7 +1710,7 @@ where top: self.top.clone() * rhs.top, right: self.right.clone() * rhs.right, bottom: self.bottom.clone() * rhs.bottom, - left: self.left * rhs.left, + left: self.left.clone() * rhs.left, } } } @@ -1957,7 +1955,7 @@ impl Edges { /// 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, rem_size: Pixels) -> Edges { + pub fn to_pixels(&self, parent_size: Size, rem_size: Pixels) -> Edges { Edges { top: self.top.to_pixels(parent_size.height, rem_size), right: self.right.to_pixels(parent_size.width, rem_size), @@ -2027,7 +2025,7 @@ impl Edges { /// 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 { + pub fn to_pixels(&self, rem_size: Pixels) -> Edges { Edges { top: self.top.to_pixels(rem_size), right: self.right.to_pixels(rem_size), @@ -2272,7 +2270,7 @@ impl Corners { /// 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 { + pub fn to_pixels(&self, rem_size: Pixels) -> Corners { Corners { top_left: self.top_left.to_pixels(rem_size), top_right: self.top_right.to_pixels(rem_size), @@ -2411,7 +2409,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 * rhs.bottom_left, + bottom_left: self.bottom_left.clone() * rhs.bottom_left, } } } @@ -2858,7 +2856,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 +3071,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 +3166,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 +3182,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 +3313,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, }, } } @@ -3524,7 +3522,7 @@ impl Serialize for Length { /// # Returns /// /// A `DefiniteLength` representing the relative length as a fraction of the parent's size. -pub const fn relative(fraction: f32) -> DefiniteLength { +pub fn relative(fraction: f32) -> DefiniteLength { DefiniteLength::Fraction(fraction) } @@ -3610,37 +3608,6 @@ impl From<()> for Length { } } -/// A location in a grid layout. -#[derive(Clone, PartialEq, Debug, Serialize, Deserialize, JsonSchema, Default)] -pub struct GridLocation { - /// The rows this item uses within the grid. - pub row: Range, - /// The columns this item uses within the grid. - pub column: Range, -} - -/// The placement of an item within a grid layout's column or row. -#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize, JsonSchema, Default)] -pub enum GridPlacement { - /// The grid line index to place this item. - Line(i16), - /// The number of grid lines to span. - Span(u16), - /// Automatically determine the placement, equivalent to Span(1) - #[default] - Auto, -} - -impl From for taffy::GridPlacement { - fn from(placement: GridPlacement) -> Self { - match placement { - GridPlacement::Line(index) => taffy::GridPlacement::from_line_index(index), - GridPlacement::Span(span) => taffy::GridPlacement::from_span(span), - GridPlacement::Auto => taffy::GridPlacement::Auto, - } - } -} - /// Provides a trait for types that can calculate half of their value. /// /// The `Half` trait is used for types that can be evenly divided, returning a new instance of the same type diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index 0f5b98df39..09799eb910 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -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::{FutureExt, Timeout, arc_cow::ArcCow}; +pub use util::arc_cow::ArcCow; pub use view::*; pub use window::*; @@ -172,10 +172,6 @@ pub trait AppContext { type Result; /// 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( &mut self, build_entity: impl FnOnce(&mut Context) -> T, @@ -352,7 +348,7 @@ impl Flatten for Result { } /// Information about the GPU GPUI is running on. -#[derive(Default, Debug, serde::Serialize, serde::Deserialize, Clone)] +#[derive(Default, Debug)] pub struct GpuSpecs { /// Whether the GPU is really a fake (like `llvmpipe`) running on the CPU. pub is_software_emulated: bool, diff --git a/crates/gpui/src/inspector.rs b/crates/gpui/src/inspector.rs index 9f86576a59..23c46edcc1 100644 --- a/crates/gpui/src/inspector.rs +++ b/crates/gpui/src/inspector.rs @@ -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(), diff --git a/crates/gpui/src/interactive.rs b/crates/gpui/src/interactive.rs index 218ae5fcdf..edd807da11 100644 --- a/crates/gpui/src/interactive.rs +++ b/crates/gpui/src/interactive.rs @@ -1,6 +1,6 @@ use crate::{ - Bounds, Capslock, Context, Empty, IntoElement, Keystroke, Modifiers, Pixels, Point, Render, - Window, point, seal::Sealed, + Capslock, Context, Empty, IntoElement, Keystroke, Modifiers, Pixels, Point, Render, Window, + point, seal::Sealed, }; use smallvec::SmallVec; use std::{any::Any, fmt::Debug, ops::Deref, path::PathBuf}; @@ -141,7 +141,7 @@ impl MouseEvent for MouseUpEvent {} /// A click event, generated when a mouse button is pressed and released. #[derive(Clone, Debug, Default)] -pub struct MouseClickEvent { +pub struct ClickEvent { /// The mouse event when the button was pressed. pub down: MouseDownEvent, @@ -149,126 +149,18 @@ pub struct MouseClickEvent { pub up: MouseUpEvent, } -/// A click event that was generated by a keyboard button being pressed and released. -#[derive(Clone, Debug, Default)] -pub struct KeyboardClickEvent { - /// The keyboard button that was pressed to trigger the click. - pub button: KeyboardButton, - - /// The bounds of the element that was clicked. - pub bounds: Bounds, -} - -/// A click event, generated when a mouse button or keyboard button is pressed and released. -#[derive(Clone, Debug)] -pub enum ClickEvent { - /// A click event trigger by a mouse button being pressed and released. - Mouse(MouseClickEvent), - /// A click event trigger by a keyboard button being pressed and released. - Keyboard(KeyboardClickEvent), -} - -impl Default for ClickEvent { - fn default() -> Self { - ClickEvent::Keyboard(KeyboardClickEvent::default()) - } -} - impl ClickEvent { - /// Returns the modifiers that were held during the click event - /// - /// `Keyboard`: The keyboard click events never have modifiers. - /// `Mouse`: Modifiers that were held during the mouse key up event. + /// Returns the modifiers that were held down during both the + /// mouse down and mouse up events pub fn modifiers(&self) -> Modifiers { - match self { - // Click events are only generated from keyboard events _without any modifiers_, so we know the modifiers are always Default - ClickEvent::Keyboard(_) => Modifiers::default(), - // Click events on the web only reflect the modifiers for the keyup event, - // tested via observing the behavior of the `ClickEvent.shiftKey` field in Chrome 138 - // under various combinations of modifiers and keyUp / keyDown events. - ClickEvent::Mouse(event) => event.up.modifiers, + Modifiers { + control: self.up.modifiers.control && self.down.modifiers.control, + alt: self.up.modifiers.alt && self.down.modifiers.alt, + shift: self.up.modifiers.shift && self.down.modifiers.shift, + platform: self.up.modifiers.platform && self.down.modifiers.platform, + function: self.up.modifiers.function && self.down.modifiers.function, } } - - /// Returns the position of the click event - /// - /// `Keyboard`: The bottom left corner of the clicked hitbox - /// `Mouse`: The position of the mouse when the button was released. - pub fn position(&self) -> Point { - match self { - ClickEvent::Keyboard(event) => event.bounds.bottom_left(), - ClickEvent::Mouse(event) => event.up.position, - } - } - - /// Returns the mouse position of the click event - /// - /// `Keyboard`: None - /// `Mouse`: The position of the mouse when the button was released. - pub fn mouse_position(&self) -> Option> { - match self { - ClickEvent::Keyboard(_) => None, - ClickEvent::Mouse(event) => Some(event.up.position), - } - } - - /// Returns if this was a right click - /// - /// `Keyboard`: false - /// `Mouse`: Whether the right button was pressed and released - pub fn is_right_click(&self) -> bool { - match self { - ClickEvent::Keyboard(_) => false, - ClickEvent::Mouse(event) => { - event.down.button == MouseButton::Right && event.up.button == MouseButton::Right - } - } - } - - /// Returns whether the click was a standard click - /// - /// `Keyboard`: Always true - /// `Mouse`: Left button pressed and released - pub fn standard_click(&self) -> bool { - match self { - ClickEvent::Keyboard(_) => true, - ClickEvent::Mouse(event) => { - event.down.button == MouseButton::Left && event.up.button == MouseButton::Left - } - } - } - - /// Returns whether the click focused the element - /// - /// `Keyboard`: false, keyboard clicks only work if an element is already focused - /// `Mouse`: Whether this was the first focusing click - pub fn first_focus(&self) -> bool { - match self { - ClickEvent::Keyboard(_) => false, - ClickEvent::Mouse(event) => event.down.first_mouse, - } - } - - /// Returns the click count of the click event - /// - /// `Keyboard`: Always 1 - /// `Mouse`: Count of clicks from MouseUpEvent - pub fn click_count(&self) -> usize { - match self { - ClickEvent::Keyboard(_) => 1, - ClickEvent::Mouse(event) => event.up.click_count, - } - } -} - -/// An enum representing the keyboard button that was pressed for a click event. -#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, Default)] -pub enum KeyboardButton { - /// Enter key was clicked - #[default] - Enter, - /// Space key was clicked - Space, } /// An enum representing the mouse button that was pressed. diff --git a/crates/gpui/src/key_dispatch.rs b/crates/gpui/src/key_dispatch.rs index 95374e579f..cc6ebb9b08 100644 --- a/crates/gpui/src/key_dispatch.rs +++ b/crates/gpui/src/key_dispatch.rs @@ -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); - (bindings, partial, context_stack) + return (bindings, partial, context_stack); } /// dispatch_key processes the keystroke @@ -611,17 +611,9 @@ impl DispatchTree { #[cfg(test)] mod tests { - use crate::{ - self as gpui, Element, ElementId, GlobalElementId, InspectorElementId, LayoutId, Style, - }; - use core::panic; - use std::{cell::RefCell, ops::Range, rc::Rc}; + use std::{cell::RefCell, rc::Rc}; - use crate::{ - Action, ActionRegistry, App, Bounds, Context, DispatchTree, FocusHandle, InputHandler, - IntoElement, KeyBinding, KeyContext, Keymap, Pixels, Point, Render, TestAppContext, - UTF16Selection, Window, - }; + use crate::{Action, ActionRegistry, DispatchTree, KeyBinding, KeyContext, Keymap}; #[derive(PartialEq, Eq)] struct TestAction; @@ -639,7 +631,10 @@ mod tests { } fn partial_eq(&self, action: &dyn Action) -> bool { - action.as_any().downcast_ref::() == Some(self) + action + .as_any() + .downcast_ref::() + .map_or(false, |a| self == a) } fn boxed_clone(&self) -> std::boxed::Box { @@ -679,165 +674,4 @@ mod tests { assert!(keybinding[0].action.partial_eq(&TestAction)) } - - #[crate::test] - fn test_input_handler_pending(cx: &mut TestAppContext) { - #[derive(Clone)] - struct CustomElement { - focus_handle: FocusHandle, - text: Rc>, - } - impl CustomElement { - fn new(cx: &mut Context) -> Self { - Self { - focus_handle: cx.focus_handle(), - text: Rc::default(), - } - } - } - impl Element for CustomElement { - type RequestLayoutState = (); - - type PrepaintState = (); - - fn id(&self) -> Option { - Some("custom".into()) - } - fn source_location(&self) -> Option<&'static panic::Location<'static>> { - None - } - fn request_layout( - &mut self, - _: Option<&GlobalElementId>, - _: Option<&InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (LayoutId, Self::RequestLayoutState) { - (window.request_layout(Style::default(), [], cx), ()) - } - fn prepaint( - &mut self, - _: Option<&GlobalElementId>, - _: Option<&InspectorElementId>, - _: Bounds, - _: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) -> Self::PrepaintState { - window.set_focus_handle(&self.focus_handle, cx); - } - fn paint( - &mut self, - _: Option<&GlobalElementId>, - _: Option<&InspectorElementId>, - _: Bounds, - _: &mut Self::RequestLayoutState, - _: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - let mut key_context = KeyContext::default(); - key_context.add("Terminal"); - window.set_key_context(key_context); - window.handle_input(&self.focus_handle, self.clone(), cx); - window.on_action(std::any::TypeId::of::(), |_, _, _, _| {}); - } - } - impl IntoElement for CustomElement { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } - } - - impl InputHandler for CustomElement { - fn selected_text_range( - &mut self, - _: bool, - _: &mut Window, - _: &mut App, - ) -> Option { - None - } - - fn marked_text_range(&mut self, _: &mut Window, _: &mut App) -> Option> { - None - } - - fn text_for_range( - &mut self, - _: Range, - _: &mut Option>, - _: &mut Window, - _: &mut App, - ) -> Option { - None - } - - fn replace_text_in_range( - &mut self, - replacement_range: Option>, - text: &str, - _: &mut Window, - _: &mut App, - ) { - if replacement_range.is_some() { - unimplemented!() - } - self.text.borrow_mut().push_str(text) - } - - fn replace_and_mark_text_in_range( - &mut self, - replacement_range: Option>, - new_text: &str, - _: Option>, - _: &mut Window, - _: &mut App, - ) { - if replacement_range.is_some() { - unimplemented!() - } - self.text.borrow_mut().push_str(new_text) - } - - fn unmark_text(&mut self, _: &mut Window, _: &mut App) {} - - fn bounds_for_range( - &mut self, - _: Range, - _: &mut Window, - _: &mut App, - ) -> Option> { - None - } - - fn character_index_for_point( - &mut self, - _: Point, - _: &mut Window, - _: &mut App, - ) -> Option { - None - } - } - impl Render for CustomElement { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - self.clone() - } - } - - cx.update(|cx| { - cx.bind_keys([KeyBinding::new("ctrl-b", TestAction, Some("Terminal"))]); - cx.bind_keys([KeyBinding::new("ctrl-b h", TestAction, Some("Terminal"))]); - }); - let (test, cx) = cx.add_window_view(|_, cx| CustomElement::new(cx)); - cx.update(|window, cx| { - window.focus(&test.read(cx).focus_handle); - window.activate_window(); - }); - cx.simulate_keystrokes("ctrl-b ["); - test.update(cx, |test, _| assert_eq!(test.text.borrow().as_str(), "[")) - } } diff --git a/crates/gpui/src/keymap.rs b/crates/gpui/src/keymap.rs index b3db09d821..83d7479a04 100644 --- a/crates/gpui/src/keymap.rs +++ b/crates/gpui/src/keymap.rs @@ -4,7 +4,7 @@ mod context; pub use binding::*; pub use context::*; -use crate::{Action, AsKeystroke, Keystroke, is_no_action}; +use crate::{Action, 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: &[impl AsKeystroke], + input: &[Keystroke], 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,6 +192,7 @@ 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 { @@ -263,7 +264,7 @@ mod tests { ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings); + keymap.add_bindings(bindings.clone()); let (result, pending) = keymap.bindings_for_input( &[Keystroke::parse("ctrl-a").unwrap()], @@ -289,7 +290,7 @@ mod tests { ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings); + keymap.add_bindings(bindings.clone()); // binding is only enabled in a specific context assert!( @@ -343,7 +344,7 @@ mod tests { ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings); + keymap.add_bindings(bindings.clone()); let space = || Keystroke::parse("space").unwrap(); let w = || Keystroke::parse("w").unwrap(); @@ -363,29 +364,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!(space_workspace.1); + assert_eq!(space_workspace.1, true); let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context()); assert!(space_editor.0.is_empty()); - assert!(!space_editor.1); + assert_eq!(space_editor.1, false); // 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!(space_w_workspace.1); + assert_eq!(space_w_workspace.1, true); let space_w_editor = keymap.bindings_for_input(&space_w, &editor_workspace_context()); assert!(space_w_editor.0.is_empty()); - assert!(!space_w_editor.1); + assert_eq!(space_w_editor.1, false); // 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!(!space_w_w_workspace.1); + assert_eq!(space_w_w_workspace.1, false); let space_w_w_editor = keymap.bindings_for_input(&space_w_w, &editor_workspace_context()); assert!(space_w_w_editor.0.is_empty()); - assert!(!space_w_w_editor.1); + assert_eq!(space_w_w_editor.1, false); // Now test what happens if we have another binding defined AFTER the NoAction // that should result in pending @@ -395,11 +396,11 @@ mod tests { KeyBinding::new("space w x", ActionAlpha {}, Some("editor")), ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings); + keymap.add_bindings(bindings.clone()); let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context()); assert!(space_editor.0.is_empty()); - assert!(space_editor.1); + assert_eq!(space_editor.1, true); // Now test what happens if we have another binding defined BEFORE the NoAction // that should result in pending @@ -409,11 +410,11 @@ mod tests { KeyBinding::new("space w w", NoAction {}, Some("editor")), ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings); + keymap.add_bindings(bindings.clone()); let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context()); assert!(space_editor.0.is_empty()); - assert!(space_editor.1); + assert_eq!(space_editor.1, true); // Now test what happens if we have another binding defined at a higher context // that should result in pending @@ -423,11 +424,11 @@ mod tests { KeyBinding::new("space w w", NoAction {}, Some("editor")), ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings); + keymap.add_bindings(bindings.clone()); let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context()); assert!(space_editor.0.is_empty()); - assert!(space_editor.1); + assert_eq!(space_editor.1, true); } #[test] @@ -438,7 +439,7 @@ mod tests { ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings); + keymap.add_bindings(bindings.clone()); // Ensure `space` results in pending input on the workspace, but not editor let (result, pending) = keymap.bindings_for_input( @@ -446,7 +447,7 @@ mod tests { &[KeyContext::parse("editor").unwrap()], ); assert!(result.is_empty()); - assert!(pending); + assert_eq!(pending, true); let bindings = [ KeyBinding::new("ctrl-w left", ActionAlpha {}, Some("editor")), @@ -454,7 +455,7 @@ mod tests { ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings); + keymap.add_bindings(bindings.clone()); // Ensure `space` results in pending input on the workspace, but not editor let (result, pending) = keymap.bindings_for_input( @@ -462,7 +463,7 @@ mod tests { &[KeyContext::parse("editor").unwrap()], ); assert_eq!(result.len(), 1); - assert!(!pending); + assert_eq!(pending, false); } #[test] @@ -473,7 +474,7 @@ mod tests { ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings); + keymap.add_bindings(bindings.clone()); // Ensure `space` results in pending input on the workspace, but not editor let (result, pending) = keymap.bindings_for_input( @@ -481,7 +482,7 @@ mod tests { &[KeyContext::parse("editor").unwrap()], ); assert!(result.is_empty()); - assert!(!pending); + assert_eq!(pending, false); } #[test] @@ -493,7 +494,7 @@ mod tests { ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings); + keymap.add_bindings(bindings.clone()); // Ensure `space` results in pending input on the workspace, but not editor let (result, pending) = keymap.bindings_for_input( @@ -504,7 +505,7 @@ mod tests { ], ); assert_eq!(result.len(), 1); - assert!(!pending); + assert_eq!(pending, false); } #[test] @@ -515,7 +516,7 @@ mod tests { ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings); + keymap.add_bindings(bindings.clone()); // Ensure `space` results in pending input on the workspace, but not editor let (result, pending) = keymap.bindings_for_input( @@ -526,7 +527,7 @@ mod tests { ], ); assert_eq!(result.len(), 0); - assert!(!pending); + assert_eq!(pending, false); } #[test] @@ -536,7 +537,7 @@ mod tests { KeyBinding::new("ctrl-x 0", ActionAlpha, Some("Workspace")), ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings); + keymap.add_bindings(bindings.clone()); let matched = keymap.bindings_for_input( &[Keystroke::parse("ctrl-x")].map(Result::unwrap), @@ -559,7 +560,7 @@ mod tests { KeyBinding::new("ctrl-x 0", NoAction, Some("Workspace")), ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings); + keymap.add_bindings(bindings.clone()); let matched = keymap.bindings_for_input( &[Keystroke::parse("ctrl-x")].map(Result::unwrap), @@ -578,7 +579,7 @@ mod tests { KeyBinding::new("ctrl-x 0", NoAction, Some("vim_mode == normal")), ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings); + keymap.add_bindings(bindings.clone()); let matched = keymap.bindings_for_input( &[Keystroke::parse("ctrl-x")].map(Result::unwrap), @@ -601,7 +602,7 @@ mod tests { KeyBinding::new("ctrl-x", ActionBeta, Some("vim_mode == normal")), ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings); + keymap.add_bindings(bindings.clone()); let matched = keymap.bindings_for_input( &[Keystroke::parse("ctrl-x")].map(Result::unwrap), @@ -628,7 +629,7 @@ mod tests { ]; let mut keymap = Keymap::default(); - keymap.add_bindings(bindings); + keymap.add_bindings(bindings.clone()); assert_bindings(&keymap, &ActionAlpha {}, &["ctrl-a"]); assert_bindings(&keymap, &ActionBeta {}, &[]); @@ -638,7 +639,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].inner.unparse()) + .map(|binding| binding.keystrokes[0].unparse()) .collect::>(); assert_eq!(actual, expected, "{:?}", action); } diff --git a/crates/gpui/src/keymap/binding.rs b/crates/gpui/src/keymap/binding.rs index a7cf9d5c54..1d3f612c5b 100644 --- a/crates/gpui/src/keymap/binding.rs +++ b/crates/gpui/src/keymap/binding.rs @@ -1,15 +1,14 @@ use std::rc::Rc; -use crate::{ - Action, AsKeystroke, DummyKeyboardMapper, InvalidKeystrokeError, KeyBindingContextPredicate, - KeybindingKeystroke, Keystroke, PlatformKeyboardMapper, SharedString, -}; +use collections::HashMap; + +use crate::{Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke, SharedString}; use smallvec::SmallVec; /// A keybinding and its associated metadata, from the keymap. pub struct KeyBinding { pub(crate) action: Box, - pub(crate) keystrokes: SmallVec<[KeybindingKeystroke; 2]>, + pub(crate) keystrokes: SmallVec<[Keystroke; 2]>, pub(crate) context_predicate: Option>, pub(crate) meta: Option, /// The json input string used when building the keybinding, if any @@ -31,17 +30,12 @@ impl Clone for KeyBinding { impl KeyBinding { /// Construct a new keybinding from the given data. Panics on parse error. pub fn new(keystrokes: &str, action: A, context: Option<&str>) -> Self { - let context_predicate = - context.map(|context| KeyBindingContextPredicate::parse(context).unwrap().into()); - Self::load( - keystrokes, - Box::new(action), - context_predicate, - false, - None, - &DummyKeyboardMapper, - ) - .unwrap() + 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() } /// Load a keybinding from the given raw data. @@ -49,22 +43,24 @@ impl KeyBinding { keystrokes: &str, action: Box, context_predicate: Option>, - use_key_equivalents: bool, + key_equivalents: Option<&HashMap>, action_input: Option, - keyboard_mapper: &dyn PlatformKeyboardMapper, ) -> std::result::Result { - let keystrokes: SmallVec<[KeybindingKeystroke; 2]> = keystrokes + let mut keystrokes: SmallVec<[Keystroke; 2]> = keystrokes .split_whitespace() - .map(|source| { - let keystroke = Keystroke::parse(source)?; - Ok(KeybindingKeystroke::new( - keystroke, - use_key_equivalents, - keyboard_mapper, - )) - }) + .map(Keystroke::parse) .collect::>()?; + 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, @@ -86,13 +82,13 @@ impl KeyBinding { } /// Check if the given keystrokes match this binding. - pub fn match_keystrokes(&self, typed: &[impl AsKeystroke]) -> Option { + pub fn match_keystrokes(&self, typed: &[Keystroke]) -> Option { if self.keystrokes.len() < typed.len() { return None; } for (target, typed) in self.keystrokes.iter().zip(typed.iter()) { - if !typed.as_keystroke().should_match(target) { + if !typed.should_match(target) { return None; } } @@ -101,7 +97,7 @@ impl KeyBinding { } /// Get the keystrokes associated with this binding - pub fn keystrokes(&self) -> &[KeybindingKeystroke] { + pub fn keystrokes(&self) -> &[Keystroke] { self.keystrokes.as_slice() } diff --git a/crates/gpui/src/keymap/context.rs b/crates/gpui/src/keymap/context.rs index 960bd1752f..f4b878ae77 100644 --- a/crates/gpui/src/keymap/context.rs +++ b/crates/gpui/src/keymap/context.rs @@ -287,7 +287,7 @@ impl KeyBindingContextPredicate { return false; } } - true + return true; } // Workspace > Pane > Editor // @@ -305,7 +305,7 @@ impl KeyBindingContextPredicate { return true; } } - false + return false; } Self::And(left, right) => { left.eval_inner(contexts, all_contexts) && right.eval_inner(contexts, all_contexts) @@ -461,8 +461,6 @@ fn skip_whitespace(source: &str) -> &str { #[cfg(test)] mod tests { - use core::slice; - use super::*; use crate as gpui; use KeyBindingContextPredicate::*; @@ -668,16 +666,20 @@ mod tests { let contexts = vec![other_context.clone(), child_context.clone()]; assert!(!predicate.eval(&contexts)); - let contexts = vec![parent_context.clone(), other_context, child_context.clone()]; + let contexts = vec![ + parent_context.clone(), + other_context.clone(), + child_context.clone(), + ]; assert!(predicate.eval(&contexts)); assert!(!predicate.eval(&[])); - assert!(!predicate.eval(slice::from_ref(&child_context))); + assert!(!predicate.eval(&[child_context.clone()])); assert!(!predicate.eval(&[parent_context])); 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])); + assert!(!zany_predicate.eval(&[child_context.clone()])); + assert!(zany_predicate.eval(&[child_context.clone(), child_context.clone()])); } #[test] @@ -688,13 +690,13 @@ mod tests { let parent_context = KeyContext::try_from("parent").unwrap(); let child_context = KeyContext::try_from("child").unwrap(); - assert!(not_predicate.eval(slice::from_ref(&workspace_context))); - assert!(!not_predicate.eval(slice::from_ref(&editor_context))); + assert!(not_predicate.eval(&[workspace_context.clone()])); + assert!(!not_predicate.eval(&[editor_context.clone()])); assert!(!not_predicate.eval(&[editor_context.clone(), workspace_context.clone()])); assert!(!not_predicate.eval(&[workspace_context.clone(), editor_context.clone()])); let complex_not = KeyBindingContextPredicate::parse("!editor && workspace").unwrap(); - assert!(complex_not.eval(slice::from_ref(&workspace_context))); + assert!(complex_not.eval(&[workspace_context.clone()])); assert!(!complex_not.eval(&[editor_context.clone(), workspace_context.clone()])); let not_mode_predicate = KeyBindingContextPredicate::parse("!(mode == full)").unwrap(); @@ -707,18 +709,18 @@ mod tests { assert!(not_mode_predicate.eval(&[other_mode_context])); 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()])); + assert!(not_descendant.eval(&[child_context.clone()])); assert!(!not_descendant.eval(&[parent_context.clone(), child_context.clone()])); 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, child_context])); + assert!(!not_descendant.eval(&[parent_context.clone()])); + assert!(!not_descendant.eval(&[child_context.clone()])); + assert!(!not_descendant.eval(&[parent_context.clone(), child_context.clone()])); let double_not = KeyBindingContextPredicate::parse("!!editor").unwrap(); - assert!(double_not.eval(slice::from_ref(&editor_context))); - assert!(!double_not.eval(slice::from_ref(&workspace_context))); + assert!(double_not.eval(&[editor_context.clone()])); + assert!(!double_not.eval(&[workspace_context.clone()])); // Test complex descendant cases let workspace_context = KeyContext::try_from("Workspace").unwrap(); @@ -752,9 +754,9 @@ mod tests { // !Workspace - shouldn't match when Workspace is in the context let not_workspace = KeyBindingContextPredicate::parse("!Workspace").unwrap(); - assert!(!not_workspace.eval(slice::from_ref(&workspace_context))); - assert!(not_workspace.eval(slice::from_ref(&pane_context))); - assert!(not_workspace.eval(slice::from_ref(&editor_context))); + assert!(!not_workspace.eval(&[workspace_context.clone()])); + assert!(not_workspace.eval(&[pane_context.clone()])); + assert!(not_workspace.eval(&[editor_context.clone()])); assert!(!not_workspace.eval(&workspace_pane_editor)); } } diff --git a/crates/gpui/src/path_builder.rs b/crates/gpui/src/path_builder.rs index 38903ea588..6c8cfddd52 100644 --- a/crates/gpui/src/path_builder.rs +++ b/crates/gpui/src/path_builder.rs @@ -278,7 +278,7 @@ impl PathBuilder { options: &StrokeOptions, ) -> Result, 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(); diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index f64710bc56..b495d70dfd 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -220,17 +220,14 @@ pub(crate) trait Platform: 'static { &self, options: PathPromptOptions, ) -> oneshot::Receiver>>>; - fn prompt_for_new_path( - &self, - directory: &Path, - suggested_name: Option<&str>, - ) -> oneshot::Receiver>>; + fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver>>; 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); fn on_reopen(&self, callback: Box); + fn on_keyboard_layout_change(&self, callback: Box); fn set_menus(&self, menus: Vec
, keymap: &Keymap); fn get_menus(&self) -> Option> { @@ -250,6 +247,7 @@ pub(crate) trait Platform: 'static { fn on_app_menu_action(&self, callback: Box); fn on_will_open_app_menu(&self, callback: Box); fn on_validate_app_menu_command(&self, callback: Box bool>); + fn keyboard_layout(&self) -> Box; fn compositor_name(&self) -> &'static str { "" @@ -270,10 +268,6 @@ pub(crate) trait Platform: 'static { fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task>; fn read_credentials(&self, url: &str) -> Task)>>>; fn delete_credentials(&self, url: &str) -> Task>; - - fn keyboard_layout(&self) -> Box; - fn keyboard_mapper(&self) -> Rc; - fn on_keyboard_layout_change(&self, callback: Box); } /// A handle to a platform's display, e.g. a monitor or laptop screen. @@ -594,7 +588,7 @@ impl PlatformTextSystem for NoopTextSystem { } fn font_id(&self, _descriptor: &Font) -> Result { - Ok(FontId(1)) + return Ok(FontId(1)); } fn font_metrics(&self, _font_id: FontId) -> FontMetrics { @@ -675,7 +669,7 @@ impl PlatformTextSystem for NoopTextSystem { } } let mut runs = Vec::default(); - if !glyphs.is_empty() { + if glyphs.len() > 0 { runs.push(ShapedRun { font_id: FontId(0), glyphs, @@ -1280,7 +1274,7 @@ pub enum WindowBackgroundAppearance { } /// The options that can be configured for a file dialog prompt -#[derive(Clone, Debug)] +#[derive(Copy, Clone, Debug)] pub struct PathPromptOptions { /// Should the prompt allow files to be selected? pub files: bool, @@ -1288,8 +1282,6 @@ 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, } /// What kind of prompt styling to show @@ -1510,7 +1502,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; } } diff --git a/crates/gpui/src/platform/app_menu.rs b/crates/gpui/src/platform/app_menu.rs index 4069fee726..2815cbdd7f 100644 --- a/crates/gpui/src/platform/app_menu.rs +++ b/crates/gpui/src/platform/app_menu.rs @@ -20,34 +20,6 @@ impl Menu { } } -/// OS menus are menus that are recognized by the operating system -/// This allows the operating system to provide specialized items for -/// these menus -pub struct OsMenu { - /// The name of the menu - pub name: SharedString, - - /// The type of menu - pub menu_type: SystemMenuType, -} - -impl OsMenu { - /// Create an OwnedOsMenu from this OsMenu - pub fn owned(self) -> OwnedOsMenu { - OwnedOsMenu { - name: self.name.to_string().into(), - menu_type: self.menu_type, - } - } -} - -/// The type of system menu -#[derive(Copy, Clone, Eq, PartialEq)] -pub enum SystemMenuType { - /// The 'Services' menu in the Application menu on macOS - Services, -} - /// The different kinds of items that can be in a menu pub enum MenuItem { /// A separator between items @@ -56,9 +28,6 @@ pub enum MenuItem { /// A submenu Submenu(Menu), - /// A menu, managed by the system (for example, the Services menu on macOS) - SystemMenu(OsMenu), - /// An action that can be performed Action { /// The name of this menu item @@ -84,14 +53,6 @@ impl MenuItem { Self::Submenu(menu) } - /// Creates a new submenu that is populated by the OS - pub fn os_submenu(name: impl Into, menu_type: SystemMenuType) -> Self { - Self::SystemMenu(OsMenu { - name: name.into(), - menu_type, - }) - } - /// Creates a new menu item that invokes an action pub fn action(name: impl Into, action: impl Action) -> Self { Self::Action { @@ -128,23 +89,10 @@ impl MenuItem { action, os_action, }, - MenuItem::SystemMenu(os_menu) => OwnedMenuItem::SystemMenu(os_menu.owned()), } } } -/// OS menus are menus that are recognized by the operating system -/// This allows the operating system to provide specialized items for -/// these menus -#[derive(Clone)] -pub struct OwnedOsMenu { - /// The name of the menu - pub name: SharedString, - - /// The type of menu - pub menu_type: SystemMenuType, -} - /// A menu of the application, either a main menu or a submenu #[derive(Clone)] pub struct OwnedMenu { @@ -163,9 +111,6 @@ pub enum OwnedMenuItem { /// A submenu Submenu(OwnedMenu), - /// A menu, managed by the system (for example, the Services menu on macOS) - SystemMenu(OwnedOsMenu), - /// An action that can be performed Action { /// The name of this menu item @@ -194,7 +139,6 @@ impl Clone for OwnedMenuItem { action: action.boxed_clone(), os_action: *os_action, }, - OwnedMenuItem::SystemMenu(os_menu) => OwnedMenuItem::SystemMenu(os_menu.clone()), } } } diff --git a/crates/gpui/src/platform/blade/blade_context.rs b/crates/gpui/src/platform/blade/blade_context.rs index 12c68a1e70..48872f1619 100644 --- a/crates/gpui/src/platform/blade/blade_context.rs +++ b/crates/gpui/src/platform/blade/blade_context.rs @@ -49,7 +49,7 @@ fn parse_pci_id(id: &str) -> anyhow::Result { "Expected a 4 digit PCI ID in hexadecimal format" ); - u32::from_str_radix(id, 16).context("parsing PCI ID as hex") + return u32::from_str_radix(id, 16).context("parsing PCI ID as hex"); } #[cfg(test)] diff --git a/crates/gpui/src/platform/blade/blade_renderer.rs b/crates/gpui/src/platform/blade/blade_renderer.rs index cc1df7748b..2e18d2be22 100644 --- a/crates/gpui/src/platform/blade/blade_renderer.rs +++ b/crates/gpui/src/platform/blade/blade_renderer.rs @@ -434,24 +434,24 @@ impl BladeRenderer { } fn wait_for_gpu(&mut self) { - 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" { + 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" + ); + } 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" + "your device information is: {:?}", + self.gpu.device_information() ); + 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) {} } } @@ -606,7 +606,7 @@ impl BladeRenderer { xy_position: v.xy_position, st_position: v.st_position, color: path.color, - bounds: path.clipped_bounds(), + bounds: path.bounds.intersect(&path.content_mask.bounds), })); } let vertex_buf = unsafe { self.instance_belt.alloc_typed(&vertices, &self.gpu) }; @@ -735,13 +735,13 @@ impl BladeRenderer { paths .iter() .map(|path| PathSprite { - bounds: path.clipped_bounds(), + bounds: path.bounds, }) .collect() } else { - let mut bounds = first_path.clipped_bounds(); + let mut bounds = first_path.bounds; for path in paths.iter().skip(1) { - bounds = bounds.union(&path.clipped_bounds()); + bounds = bounds.union(&path.bounds); } vec![PathSprite { bounds }] }; diff --git a/crates/gpui/src/platform/blade/shaders.wgsl b/crates/gpui/src/platform/blade/shaders.wgsl index 95980b54fe..b1ffb1812e 100644 --- a/crates/gpui/src/platform/blade/shaders.wgsl +++ b/crates/gpui/src/platform/blade/shaders.wgsl @@ -1057,9 +1057,6 @@ fn vs_underline(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) @fragment fn fs_underline(input: UnderlineVarying) -> @location(0) vec4 { - const WAVE_FREQUENCY: f32 = 2.0; - const WAVE_HEIGHT_RATIO: f32 = 0.8; - // Alpha clip first, since we don't have `clip_distance`. if (any(input.clip_distances < vec4(0.0))) { return vec4(0.0); @@ -1072,11 +1069,9 @@ fn fs_underline(input: UnderlineVarying) -> @location(0) vec4 { } let half_thickness = underline.thickness * 0.5; - let st = (input.position.xy - underline.bounds.origin) / underline.bounds.size.y - vec2(0.0, 0.5); - let frequency = M_PI_F * WAVE_FREQUENCY * underline.thickness / underline.bounds.size.y; - let amplitude = (underline.thickness * WAVE_HEIGHT_RATIO) / underline.bounds.size.y; - + let frequency = M_PI_F * 3.0 * underline.thickness / 3.0; + let amplitude = 1.0 / (4.0 * underline.thickness); let sine = sin(st.x * frequency) * amplitude; let dSine = cos(st.x * frequency) * amplitude * frequency; let distance = (st.y - sine) / sqrt(1.0 + dSine * dSine); diff --git a/crates/gpui/src/platform/keyboard.rs b/crates/gpui/src/platform/keyboard.rs index 10b8620258..e28d781520 100644 --- a/crates/gpui/src/platform/keyboard.rs +++ b/crates/gpui/src/platform/keyboard.rs @@ -1,7 +1,3 @@ -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 @@ -9,33 +5,3 @@ 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>; -} - -/// 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> { - None - } -} diff --git a/crates/gpui/src/platform/keystroke.rs b/crates/gpui/src/platform/keystroke.rs index 6ce17c3a01..24601eefd6 100644 --- a/crates/gpui/src/platform/keystroke.rs +++ b/crates/gpui/src/platform/keystroke.rs @@ -5,14 +5,6 @@ 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 { @@ -32,17 +24,6 @@ pub struct Keystroke { pub key_char: Option, } -/// 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. - pub inner: Keystroke, - /// The modifiers to display. - pub display_modifiers: Modifiers, - /// The key to display. - pub 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)] @@ -77,7 +58,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: &KeybindingKeystroke) -> bool { + pub fn should_match(&self, target: &Keystroke) -> bool { #[cfg(not(target_os = "windows"))] if let Some(key_char) = self .key_char @@ -90,7 +71,7 @@ impl Keystroke { ..Default::default() }; - if &target.inner.key == key_char && target.inner.modifiers == ime_modifiers { + if &target.key == key_char && target.modifiers == ime_modifiers { return true; } } @@ -102,12 +83,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.inner.key == key_char && target.inner.modifiers == Modifiers::none() { + if &target.key == key_char && target.modifiers == Modifiers::none() { return true; } } - target.inner.modifiers == self.modifiers && target.inner.key == self.key + target.modifiers == self.modifiers && target.key == self.key } /// key syntax is: @@ -219,7 +200,31 @@ impl Keystroke { /// Produces a representation of this key that Parse can understand. pub fn unparse(&self) -> String { - unparse(&self.modifiers, &self.key) + 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 } /// Returns true if this keystroke left @@ -261,32 +266,6 @@ impl Keystroke { } } -impl KeybindingKeystroke { - /// Create a new keybinding keystroke from the given keystroke - pub fn new( - inner: Keystroke, - use_key_equivalents: bool, - keyboard_mapper: &dyn PlatformKeyboardMapper, - ) -> Self { - keyboard_mapper.map_key_equivalent(inner, use_key_equivalents) - } - - pub(crate) fn from_keystroke(keystroke: Keystroke) -> Self { - let key = keystroke.key.clone(); - let modifiers = keystroke.modifiers; - KeybindingKeystroke { - inner: keystroke, - display_modifiers: modifiers, - display_key: key, - } - } - - /// Produces a representation of this key that Parse can understand. - pub fn unparse(&self) -> String { - unparse(&self.display_modifiers, &self.display_key) - } -} - fn is_printable_key(key: &str) -> bool { !matches!( key, @@ -343,15 +322,65 @@ fn is_printable_key(key: &str) -> bool { impl std::fmt::Display for Keystroke { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - display_modifiers(&self.modifiers, f)?; - display_key(&self.key, f) - } -} + if self.modifiers.control { + #[cfg(target_os = "macos")] + f.write_char('^')?; -impl std::fmt::Display for KeybindingKeystroke { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - display_modifiers(&self.display_modifiers, f)?; - display_key(&self.display_key, f) + #[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('⇧')?; + + #[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) } } @@ -571,110 +600,3 @@ 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 -} diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index 8bd89fc399..fe6a36baa8 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -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, PlatformKeyboardMapper, - PlatformTextSystem, PlatformWindow, Point, Result, Task, WindowAppearance, WindowParams, px, + Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, 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)); + let dispatcher = Arc::new(LinuxDispatcher::new(main_sender.clone())); let background_executor = BackgroundExecutor::new(dispatcher.clone()); let common = LinuxCommon { background_executor, - foreground_executor: ForegroundExecutor::new(dispatcher), + foreground_executor: ForegroundExecutor::new(dispatcher.clone()), text_system, appearance: WindowAppearance::Light, auto_hide_scrollbars: false, @@ -144,10 +144,6 @@ impl Platform for P { self.keyboard_layout() } - fn keyboard_mapper(&self) -> Rc { - Rc::new(crate::DummyKeyboardMapper) - } - fn on_keyboard_layout_change(&self, callback: Box) { self.with_common(|common| common.callbacks.keyboard_layout_change = Some(callback)); } @@ -298,7 +294,6 @@ impl 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() @@ -332,35 +327,26 @@ impl Platform for P { done_rx } - fn prompt_for_new_path( - &self, - directory: &Path, - suggested_name: Option<&str>, - ) -> oneshot::Receiver>> { + fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver>> { let (done_tx, done_rx) = oneshot::channel(); #[cfg(not(any(feature = "wayland", feature = "x11")))] - let _ = (done_tx.send(Ok(None)), directory, suggested_name); + let _ = (done_tx.send(Ok(None)), directory); #[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 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 { + 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 + { Ok(request) => request, Err(err) => { let result = match err { @@ -445,7 +431,7 @@ impl Platform for P { fn app_path(&self) -> Result { // get the path of the executable of the current process let app_path = env::current_exe()?; - Ok(app_path) + return Ok(app_path); } fn set_menus(&self, menus: Vec, _keymap: &Keymap) { @@ -646,7 +632,7 @@ pub(super) fn get_xkb_compose_state(cx: &xkb::Context) -> Option = 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, @@ -671,7 +657,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 { @@ -994,18 +980,21 @@ mod tests { #[test] fn test_is_within_click_distance() { let zero = Point::new(px(0.0), px(0.0)); - 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)) - ),); + 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 + ); } } diff --git a/crates/gpui/src/platform/linux/text_system.rs b/crates/gpui/src/platform/linux/text_system.rs index f66a2e71d4..e6f6e9a680 100644 --- a/crates/gpui/src/platform/linux/text_system.rs +++ b/crates/gpui/src/platform/linux/text_system.rs @@ -213,7 +213,11 @@ impl CosmicTextSystemState { features: &FontFeatures, ) -> Result> { // TODO: Determine the proper system UI font. - let name = crate::text_system::font_name_with_fallbacks(name, "IBM Plex Sans"); + let name = if name == ".SystemUIFont" { + "Zed Plex Sans" + } else { + name + }; let families = self .font_system diff --git a/crates/gpui/src/platform/linux/wayland.rs b/crates/gpui/src/platform/linux/wayland.rs index 487bc9f38c..cf73832b11 100644 --- a/crates/gpui/src/platform/linux/wayland.rs +++ b/crates/gpui/src/platform/linux/wayland.rs @@ -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, diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index 189cfa1954..72e4477ecf 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -359,13 +359,13 @@ impl WaylandClientStatePtr { } changed }; - - 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); + 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); + } } } @@ -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() - && !window.ptr_eq(&closed_window) - { - state.mouse_focused_window = Some(window); + 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.keyboard_focused_window.take() - && !window.ptr_eq(&closed_window) - { - state.keyboard_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 state.windows.is_empty() { state.common.signal.stop(); @@ -528,7 +528,7 @@ impl WaylandClient { client.common.appearance = appearance; - for window in client.windows.values_mut() { + for (_, window) in &mut client.windows { window.set_appearance(appearance); } } @@ -710,7 +710,9 @@ impl LinuxClient for WaylandClient { fn set_cursor_style(&self, style: CursorStyle) { let mut state = self.0.borrow_mut(); - let need_update = state.cursor_style != Some(style); + let need_update = state + .cursor_style + .map_or(true, |current_style| current_style != style); if need_update { let serial = state.serial_tracker.get(SerialKind::MouseEnter); @@ -949,8 +951,11 @@ impl Dispatch for WaylandClientStatePtr { }; drop(state); - if let wl_callback::Event::Done { .. } = event { - window.frame(); + match event { + wl_callback::Event::Done { .. } => { + window.frame(); + } + _ => {} } } } @@ -1140,7 +1145,7 @@ impl Dispatch 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(); @@ -1280,6 +1285,7 @@ impl Dispatch 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); @@ -1288,7 +1294,7 @@ impl Dispatch 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() { @@ -1532,9 +1538,12 @@ impl Dispatch 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); @@ -1571,7 +1580,7 @@ impl Dispatch for WaylandClientStatePtr { if state .keyboard_focused_window .as_ref() - .is_some_and(|keyboard_window| window.ptr_eq(keyboard_window)) + .map_or(false, |keyboard_window| window.ptr_eq(&keyboard_window)) { state.enter_token = None; } @@ -1778,17 +1787,17 @@ impl Dispatch for WaylandClientStatePtr { 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); + } 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); + } } } } @@ -2010,22 +2019,25 @@ impl Dispatch for WaylandClientStatePtr { let client = this.get_client(); let mut state = client.borrow_mut(); - 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)); - } + 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)); + } - // 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); + } } + _ => {} } } } @@ -2106,10 +2118,13 @@ impl Dispatch let client = this.get_client(); let mut state = client.borrow_mut(); - 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); + 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); + } + } + _ => {} } } } diff --git a/crates/gpui/src/platform/linux/wayland/cursor.rs b/crates/gpui/src/platform/linux/wayland/cursor.rs index c7c9139dea..2a24d0e1ba 100644 --- a/crates/gpui/src/platform/linux/wayland/cursor.rs +++ b/crates/gpui/src/platform/linux/wayland/cursor.rs @@ -45,11 +45,10 @@ impl Cursor { } fn set_theme_internal(&mut self, theme_name: Option) { - if let Some(loaded_theme) = self.loaded_theme.as_ref() - && loaded_theme.name == theme_name - && loaded_theme.scaled_size == self.scaled_size - { - return; + if let Some(loaded_theme) = self.loaded_theme.as_ref() { + if 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( @@ -67,7 +66,7 @@ impl Cursor { { self.loaded_theme = Some(LoadedTheme { theme, - name: theme_name, + name: theme_name.map(|name| name.to_string()), scaled_size: self.scaled_size, }); } @@ -145,7 +144,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(); } diff --git a/crates/gpui/src/platform/linux/wayland/window.rs b/crates/gpui/src/platform/linux/wayland/window.rs index 7570c58c09..2b2207e22c 100644 --- a/crates/gpui/src/platform/linux/wayland/window.rs +++ b/crates/gpui/src/platform/linux/wayland/window.rs @@ -355,82 +355,85 @@ impl WaylandWindowStatePtr { } pub fn handle_xdg_surface_event(&self, event: xdg_surface::Event) { - 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; + 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(); - } - } - } - { - 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 callbacks = self.callbacks.borrow_mut(); + if let Some(appearance_changed) = callbacks.appearance_changed.as_mut() { + appearance_changed(); } } - drop(state); - if let Some(size) = configure.size { - self.resize(size); + } + { + 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); + 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(); + } } - 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) { - if let zxdg_toplevel_decoration_v1::Event::Configure { mode } = event { - match mode { + match event { + zxdg_toplevel_decoration_v1::Event::Configure { mode } => match mode { WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ServerSide) => { self.state.borrow_mut().decorations = WindowDecorations::Server; if let Some(mut appearance_changed) = @@ -454,13 +457,17 @@ impl WaylandWindowStatePtr { WEnum::Unknown(v) => { log::warn!("Unknown decoration mode: {}", v); } - } + }, + _ => {} } } pub fn handle_fractional_scale_event(&self, event: wp_fractional_scale_v1::Event) { - if let wp_fractional_scale_v1::Event::PreferredScale { scale } = event { - self.rescale(scale as f32 / 120.0); + match event { + wp_fractional_scale_v1::Event::PreferredScale { scale } => { + self.rescale(scale as f32 / 120.0); + } + _ => {} } } @@ -662,8 +669,8 @@ impl WaylandWindowStatePtr { pub fn set_size_and_scale(&self, size: Option>, scale: Option) { let (size, scale) = { let mut state = self.state.borrow_mut(); - if size.is_none_or(|size| size == state.bounds.size) - && scale.is_none_or(|scale| scale == state.scale) + if size.map_or(true, |size| size == state.bounds.size) + && scale.map_or(true, |scale| scale == state.scale) { return; } @@ -706,20 +713,21 @@ impl WaylandWindowStatePtr { } pub fn handle_input(&self, input: PlatformInput) { - if let Some(ref mut fun) = self.callbacks.borrow_mut().input - && !fun(input.clone()).propagate - { - return; + if let Some(ref mut fun) = self.callbacks.borrow_mut().input { + if !fun(input.clone()).propagate { + return; + } } - 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); + 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); + } + } } } } @@ -1139,7 +1147,7 @@ fn update_window(mut state: RefMut) { } 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, @@ -1148,7 +1156,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, diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index 9a43bd6470..573e4addf7 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -232,12 +232,15 @@ impl X11ClientStatePtr { }; let mut state = client.0.borrow_mut(); - 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 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 state.mouse_focused_window == Some(x_window) { state.mouse_focused_window = None; @@ -456,7 +459,7 @@ impl X11Client { move |event, _, client| match event { XDPEvent::WindowAppearance(appearance) => { client.with_common(|common| common.appearance = appearance); - for window in client.0.borrow_mut().windows.values_mut() { + for (_, window) in &mut client.0.borrow_mut().windows { window.window.set_appearance(appearance); } } @@ -562,10 +565,10 @@ impl X11Client { events.push(last_keymap_change_event); } - if let Some(last_press) = last_key_press.as_ref() - && last_press.detail == key_press.detail - { - continue; + if let Some(last_press) = last_key_press.as_ref() { + if last_press.detail == key_press.detail { + continue; + } } if let Some(Event::KeyRelease(key_release)) = @@ -639,7 +642,13 @@ impl X11Client { let xim_connected = xim_handler.connected; drop(state); - let xim_filtered = ximc.filter_event(&event, &mut xim_handler); + let xim_filtered = match ximc.filter_event(&event, &mut xim_handler) { + Ok(handled) => handled, + Err(err) => { + log::error!("XIMClientError: {}", err); + false + } + }; let xim_callback_event = xim_handler.last_callback_event.take(); let mut state = self.0.borrow_mut(); @@ -650,28 +659,14 @@ impl X11Client { self.handle_xim_callback_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); - } + if xim_filtered { + continue; + } + + if xim_connected { + self.xim_handle_event(event); + } else { + self.handle_event(event); } } } @@ -873,19 +868,22 @@ impl X11Client { let Some(reply) = reply else { return Some(()); }; - 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; + 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(_) => {} } } Event::ConfigureNotify(event) => { @@ -1206,7 +1204,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(pointer, &event); + let scroll_delta = get_scroll_delta_and_update_state(&mut pointer, &event); drop(state); if let Some(scroll_delta) = scroll_delta { window.handle_input(PlatformInput::ScrollWheel(make_scroll_wheel_event( @@ -1265,7 +1263,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(pointer); + reset_pointer_device_scroll_positions(&mut pointer); } } _ => {} @@ -1329,7 +1327,7 @@ impl X11Client { state.composing = false; drop(state); if let Some(mut keystroke) = keystroke { - keystroke.key_char = Some(text); + keystroke.key_char = Some(text.clone()); window.handle_input(PlatformInput::KeyDown(crate::KeyDownEvent { keystroke, is_held: false, @@ -1580,11 +1578,11 @@ impl LinuxClient for X11Client { fn read_from_primary(&self) -> Option { let state = self.0.borrow_mut(); - state + return 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 { @@ -1597,11 +1595,11 @@ impl LinuxClient for X11Client { { return state.clipboard_item.clone(); } - state + return 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) { @@ -2004,12 +2002,12 @@ fn check_gtk_frame_extents_supported( } fn xdnd_is_atom_supported(atom: u32, atoms: &XcbAtoms) -> bool { - atom == atoms.TEXT + return 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( @@ -2029,15 +2027,16 @@ fn xdnd_get_supported_atom( ), ) .log_with_level(Level::Warn) - && let Some(atoms) = reply.value32() { - for atom in atoms { - if xdnd_is_atom_supported(atom, supported_atoms) { - return atom; + if let Some(atoms) = reply.value32() { + for atom in atoms { + if xdnd_is_atom_supported(atom, &supported_atoms) { + return atom; + } } } } - 0 + return 0; } fn xdnd_send_finished( @@ -2108,7 +2107,7 @@ fn current_pointer_device_states( .classes .iter() .filter_map(|class| class.data.as_scroll()) - .copied() + .map(|class| *class) .rev() .collect::>(); let old_state = scroll_values_to_preserve.get(&info.deviceid); @@ -2138,7 +2137,7 @@ fn current_pointer_device_states( if pointer_device_states.is_empty() { log::error!("Found no xinput mouse pointers."); } - Some(pointer_device_states) + return Some(pointer_device_states); } /// Returns true if the device is a pointer device. Does not include pointer device groups. @@ -2404,13 +2403,11 @@ fn legacy_get_randr_scale_factor(connection: &XCBConnection, root: u32) -> Optio let mut crtc_infos: HashMap = HashMap::default(); let mut valid_outputs: HashSet = HashSet::new(); for (crtc, cookie) in crtc_cookies { - 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); + 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); + } } } diff --git a/crates/gpui/src/platform/linux/x11/clipboard.rs b/crates/gpui/src/platform/linux/x11/clipboard.rs index a6f96d38c4..5d42eadaaf 100644 --- a/crates/gpui/src/platform/linux/x11/clipboard.rs +++ b/crates/gpui/src/platform/linux/x11/clipboard.rs @@ -1078,11 +1078,11 @@ impl Clipboard { } else { String::from_utf8(result.bytes).map_err(|_| Error::ConversionFailure)? }; - Ok(ClipboardItem::new_string(text)) + return Ok(ClipboardItem::new_string(text)); } pub fn is_owner(&self, selection: ClipboardKind) -> bool { - self.inner.is_owner(selection).unwrap_or(false) + return 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 - && 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::() { - 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 { + 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::() { + 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."); + } } } } diff --git a/crates/gpui/src/platform/linux/x11/event.rs b/crates/gpui/src/platform/linux/x11/event.rs index 17bcc908d3..cd4cef24a3 100644 --- a/crates/gpui/src/platform/linux/x11/event.rs +++ b/crates/gpui/src/platform/linux/x11/event.rs @@ -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, bit_index: u16) -> bool { let array_index = bit_index as usize / 32; bit_vec .get(array_index) - .is_some_and(|bits| bit_is_set(*bits, bit_index % 32)) + .map_or(false, |bits| bit_is_set(*bits, bit_index % 32)) } fn bit_is_set(bits: u32, bit_index: u16) -> bool { diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/crates/gpui/src/platform/linux/x11/window.rs index 6af943b317..1a3c323c35 100644 --- a/crates/gpui/src/platform/linux/x11/window.rs +++ b/crates/gpui/src/platform/linux/x11/window.rs @@ -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 - && 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 { + 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 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 - && !fun(input.clone()).propagate - { - return; + if let Some(ref mut fun) = self.callbacks.borrow_mut().input { + if !fun(input.clone()).propagate { + return; + } } if let PlatformInput::KeyDown(event) = input { // only allow shift modifier when inserting text @@ -1068,14 +1068,15 @@ impl X11WindowStatePtr { } let mut callbacks = self.callbacks.borrow_mut(); - if let Some((content_size, scale_factor)) = resize_args - && let Some(ref mut fun) = callbacks.resize - { - fun(content_size, scale_factor) + if let Some((content_size, scale_factor)) = resize_args { + if let Some(ref mut fun) = callbacks.resize { + fun(content_size, scale_factor) + } } - - if !is_resize && let Some(ref mut fun) = callbacks.moved { - fun(); + if !is_resize { + if let Some(ref mut fun) = callbacks.moved { + fun(); + } } Ok(()) diff --git a/crates/gpui/src/platform/mac/events.rs b/crates/gpui/src/platform/mac/events.rs index 938db4b762..0dc361b9dc 100644 --- a/crates/gpui/src/platform/mac/events.rs +++ b/crates/gpui/src/platform/mac/events.rs @@ -311,8 +311,9 @@ 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 - .is_none_or(|ch| !(NSUpArrowFunctionKey..=NSModeSwitchFunctionKey).contains(&ch)); + && first_char.map_or(true, |ch| { + !(NSUpArrowFunctionKey..=NSModeSwitchFunctionKey).contains(&ch) + }); #[allow(non_upper_case_globals)] let key = match first_char { @@ -426,7 +427,7 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke { key_char = Some(chars_for_modified_key(native_event.keyCode(), mods)); } - if shift + let mut key = if shift && chars_ignoring_modifiers .chars() .all(|c| c.is_ascii_lowercase()) @@ -437,7 +438,9 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke { chars_with_shift } else { chars_ignoring_modifiers - } + }; + + key } }; diff --git a/crates/gpui/src/platform/mac/keyboard.rs b/crates/gpui/src/platform/mac/keyboard.rs index 1409731246..a9f6af3edb 100644 --- a/crates/gpui/src/platform/mac/keyboard.rs +++ b/crates/gpui/src/platform/mac/keyboard.rs @@ -1,9 +1,8 @@ -use collections::HashMap; use std::ffi::{CStr, c_void}; use objc::{msg_send, runtime::Object, sel, sel_impl}; -use crate::{KeybindingKeystroke, Keystroke, PlatformKeyboardLayout, PlatformKeyboardMapper}; +use crate::PlatformKeyboardLayout; use super::{ TISCopyCurrentKeyboardLayoutInputSource, TISGetInputSourceProperty, kTISPropertyInputSourceID, @@ -15,10 +14,6 @@ pub(crate) struct MacKeyboardLayout { name: String, } -pub(crate) struct MacKeyboardMapper { - key_equivalents: Option>, -} - impl PlatformKeyboardLayout for MacKeyboardLayout { fn id(&self) -> &str { &self.id @@ -29,27 +24,6 @@ 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> { - self.key_equivalents.as_ref() - } -} - impl MacKeyboardLayout { pub(crate) fn new() -> Self { unsafe { @@ -73,1428 +47,3 @@ 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> { - 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())) -} diff --git a/crates/gpui/src/platform/mac/metal_renderer.rs b/crates/gpui/src/platform/mac/metal_renderer.rs index 9e5d6ec5ff..fb5cb852d6 100644 --- a/crates/gpui/src/platform/mac/metal_renderer.rs +++ b/crates/gpui/src/platform/mac/metal_renderer.rs @@ -314,15 +314,6 @@ impl MetalRenderer { } fn update_path_intermediate_textures(&mut self, size: Size) { - // 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); @@ -332,7 +323,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; + let mut msaa_descriptor = texture_descriptor.clone(); 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 _); @@ -445,14 +436,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(); @@ -480,7 +471,7 @@ impl MetalRenderer { instance_buffer, &mut instance_offset, viewport_size, - command_encoder, + &command_encoder, ) } else { false @@ -491,7 +482,7 @@ impl MetalRenderer { instance_buffer, &mut instance_offset, viewport_size, - command_encoder, + &command_encoder, ), PrimitiveBatch::MonochromeSprites { texture_id, @@ -502,7 +493,7 @@ impl MetalRenderer { instance_buffer, &mut instance_offset, viewport_size, - command_encoder, + &command_encoder, ), PrimitiveBatch::PolychromeSprites { texture_id, @@ -513,14 +504,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 { @@ -763,7 +754,7 @@ impl MetalRenderer { viewport_size: Size, command_encoder: &metal::RenderCommandEncoderRef, ) -> bool { - let Some(first_path) = paths.first() else { + let Some(ref first_path) = paths.first() else { return true; }; @@ -800,13 +791,13 @@ impl MetalRenderer { sprites = paths .iter() .map(|path| PathSprite { - bounds: path.clipped_bounds(), + bounds: path.bounds, }) .collect(); } else { - let mut bounds = first_path.clipped_bounds(); + let mut bounds = first_path.bounds; for path in paths.iter().skip(1) { - bounds = bounds.union(&path.clipped_bounds()); + bounds = bounds.union(&path.bounds); } sprites = vec![PathSprite { bounds }]; } diff --git a/crates/gpui/src/platform/mac/open_type.rs b/crates/gpui/src/platform/mac/open_type.rs index 37a29559fd..2ae5e8f87a 100644 --- a/crates/gpui/src/platform/mac/open_type.rs +++ b/crates/gpui/src/platform/mac/open_type.rs @@ -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 - && !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 { + if !fallbacks.fallback_list().is_empty() { + keys.push(kCTFontCascadeListAttribute); + values.push(generate_fallback_array( + fallbacks, + font.native_font().as_concrete_TypeRef(), + )); + } } let attrs = CFDictionaryCreate( kCFAllocatorDefault, diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 30453def00..1d2146cf73 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -1,5 +1,5 @@ use super::{ - BoolExt, MacKeyboardLayout, MacKeyboardMapper, + BoolExt, MacKeyboardLayout, attributed_string::{NSAttributedString, NSMutableAttributedString}, events::key_to_native, renderer, @@ -7,10 +7,9 @@ use super::{ 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, PlatformKeyboardMapper, PlatformTextSystem, - PlatformWindow, Result, SemanticVersion, SystemMenuType, Task, WindowAppearance, WindowParams, - hash, + MacDisplay, MacWindow, Menu, MenuItem, OwnedMenu, PathPromptOptions, Platform, PlatformDisplay, + PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, Result, SemanticVersion, Task, + WindowAppearance, WindowParams, hash, }; use anyhow::{Context as _, anyhow}; use block::ConcreteBlock; @@ -48,7 +47,7 @@ use objc::{ use parking_lot::Mutex; use ptr::null_mut; use std::{ - cell::Cell, + cell::{Cell, LazyCell}, convert::TryInto, ffi::{CStr, OsStr, c_void}, os::{raw::c_char, unix::ffi::OsStrExt}, @@ -57,7 +56,7 @@ use std::{ ptr, rc::Rc, slice, str, - sync::{Arc, OnceLock}, + sync::Arc, }; use strum::IntoEnumIterator; use util::ResultExt; @@ -172,7 +171,6 @@ pub(crate) struct MacPlatformState { finish_launching: Option>, dock_menu: Option, menus: Option>, - keyboard_mapper: Rc, } impl Default for MacPlatform { @@ -191,9 +189,6 @@ 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, @@ -214,7 +209,6 @@ impl MacPlatform { dock_menu: None, on_keyboard_layout_change: None, menus: None, - keyboard_mapper, })) } @@ -302,7 +296,18 @@ impl MacPlatform { actions: &mut Vec>, keymap: &Keymap, ) -> id { - static DEFAULT_CONTEXT: OnceLock> = OnceLock::new(); + const DEFAULT_CONTEXT: LazyCell> = LazyCell::new(|| { + let mut workspace_context = KeyContext::new_with_defaults(); + workspace_context.add("Workspace"); + let mut pane_context = KeyContext::new_with_defaults(); + pane_context.add("Pane"); + let mut editor_context = KeyContext::new_with_defaults(); + editor_context.add("Editor"); + + pane_context.extend(&editor_context); + workspace_context.extend(&pane_context); + vec![workspace_context] + }); unsafe { match item { @@ -318,20 +323,9 @@ impl MacPlatform { let keystrokes = keymap .bindings_for_action(action.as_ref()) .find_or_first(|binding| { - binding.predicate().is_none_or(|predicate| { - predicate.eval(DEFAULT_CONTEXT.get_or_init(|| { - let mut workspace_context = KeyContext::new_with_defaults(); - workspace_context.add("Workspace"); - let mut pane_context = KeyContext::new_with_defaults(); - pane_context.add("Pane"); - let mut editor_context = KeyContext::new_with_defaults(); - editor_context.add("Editor"); - - pane_context.extend(&editor_context); - workspace_context.extend(&pane_context); - vec![workspace_context] - })) - }) + binding + .predicate() + .is_none_or(|predicate| predicate.eval(&DEFAULT_CONTEXT)) }) .map(|binding| binding.keystrokes()); @@ -354,19 +348,19 @@ impl MacPlatform { let mut mask = NSEventModifierFlags::empty(); for (modifier, flag) in &[ ( - keystroke.display_modifiers.platform, + keystroke.modifiers.platform, NSEventModifierFlags::NSCommandKeyMask, ), ( - keystroke.display_modifiers.control, + keystroke.modifiers.control, NSEventModifierFlags::NSControlKeyMask, ), ( - keystroke.display_modifiers.alt, + keystroke.modifiers.alt, NSEventModifierFlags::NSAlternateKeyMask, ), ( - keystroke.display_modifiers.shift, + keystroke.modifiers.shift, NSEventModifierFlags::NSShiftKeyMask, ), ] { @@ -377,9 +371,9 @@ impl MacPlatform { item = NSMenuItem::alloc(nil) .initWithTitle_action_keyEquivalent_( - ns_string(name), + ns_string(&name), selector, - ns_string(key_to_native(&keystroke.display_key).as_ref()), + ns_string(key_to_native(&keystroke.key).as_ref()), ) .autorelease(); if Self::os_version() >= SemanticVersion::new(12, 0, 0) { @@ -389,7 +383,7 @@ impl MacPlatform { } else { item = NSMenuItem::alloc(nil) .initWithTitle_action_keyEquivalent_( - ns_string(name), + ns_string(&name), selector, ns_string(""), ) @@ -398,7 +392,7 @@ impl MacPlatform { } else { item = NSMenuItem::alloc(nil) .initWithTitle_action_keyEquivalent_( - ns_string(name), + ns_string(&name), selector, ns_string(""), ) @@ -418,21 +412,10 @@ impl MacPlatform { submenu.addItem_(Self::create_menu_item(item, delegate, actions, keymap)); } item.setSubmenu_(submenu); - item.setTitle_(ns_string(name)); - item - } - MenuItem::SystemMenu(OsMenu { name, menu_type }) => { - let item = NSMenuItem::new(nil).autorelease(); - let submenu = NSMenu::new(nil).autorelease(); - submenu.setDelegate_(delegate); - item.setSubmenu_(submenu); - item.setTitle_(ns_string(name)); - - match menu_type { - SystemMenuType::Services => { - let app: id = msg_send![APP_CLASS, sharedApplication]; - app.setServicesMenu_(item); - } + item.setTitle_(ns_string(&name)); + if name == "Services" { + let app: id = msg_send![APP_CLASS, sharedApplication]; + app.setServicesMenu_(item); } item @@ -711,7 +694,6 @@ 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)); @@ -721,10 +703,10 @@ impl Platform for MacPlatform { let urls = panel.URLs(); for i in 0..urls.count() { let url = urls.objectAtIndex(i); - if url.isFileURL() == YES - && let Ok(path) = ns_url_to_path(url) - { - result.push(path) + if url.isFileURL() == YES { + if let Ok(path) = ns_url_to_path(url) { + result.push(path) + } } } Some(result) @@ -737,11 +719,6 @@ 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]; } }) @@ -749,13 +726,8 @@ impl Platform for MacPlatform { done_rx } - fn prompt_for_new_path( - &self, - directory: &Path, - suggested_name: Option<&str>, - ) -> oneshot::Receiver>> { + fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver>> { 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 { @@ -765,11 +737,6 @@ 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; @@ -792,18 +759,17 @@ 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]) - && 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]) { + 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); + } } - result + return result; }) } } @@ -888,10 +854,6 @@ impl Platform for MacPlatform { Box::new(MacKeyboardLayout::new()) } - fn keyboard_mapper(&self) -> Rc { - self.0.lock().keyboard_mapper.clone() - } - fn app_path(&self) -> Result { unsafe { let bundle: id = NSBundle::mainBundle(); @@ -1403,8 +1365,6 @@ 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(); diff --git a/crates/gpui/src/platform/mac/shaders.metal b/crates/gpui/src/platform/mac/shaders.metal index 83c978b853..f9d5bdbf4c 100644 --- a/crates/gpui/src/platform/mac/shaders.metal +++ b/crates/gpui/src/platform/mac/shaders.metal @@ -567,20 +567,15 @@ vertex UnderlineVertexOutput underline_vertex( fragment float4 underline_fragment(UnderlineFragmentInput input [[stage_in]], constant Underline *underlines [[buffer(UnderlineInputIndex_Underlines)]]) { - const float WAVE_FREQUENCY = 2.0; - const float WAVE_HEIGHT_RATIO = 0.8; - Underline underline = underlines[input.underline_id]; if (underline.wavy) { float half_thickness = underline.thickness * 0.5; float2 origin = float2(underline.bounds.origin.x, underline.bounds.origin.y); - float2 st = ((input.position.xy - origin) / underline.bounds.size.height) - float2(0., 0.5); - float frequency = (M_PI_F * WAVE_FREQUENCY * underline.thickness) / underline.bounds.size.height; - float amplitude = (underline.thickness * WAVE_HEIGHT_RATIO) / underline.bounds.size.height; - + float frequency = (M_PI_F * (3. * underline.thickness)) / 8.; + float amplitude = 1. / (2. * underline.thickness); float sine = sin(st.x * frequency) * amplitude; float dSine = cos(st.x * frequency) * amplitude * frequency; float distance = (st.y - sine) / sqrt(1. + dSine * dSine); diff --git a/crates/gpui/src/platform/mac/text_system.rs b/crates/gpui/src/platform/mac/text_system.rs index 72a0f2e565..c45888bce7 100644 --- a/crates/gpui/src/platform/mac/text_system.rs +++ b/crates/gpui/src/platform/mac/text_system.rs @@ -211,7 +211,11 @@ impl MacTextSystemState { features: &FontFeatures, fallbacks: Option<&FontFallbacks>, ) -> Result> { - let name = crate::text_system::font_name_with_fallbacks(name, ".AppleSystemUIFont"); + let name = if name == ".SystemUIFont" { + ".AppleSystemUIFont" + } else { + name + }; let mut font_ids = SmallVec::new(); let family = self @@ -319,7 +323,7 @@ impl MacTextSystemState { fn is_emoji(&self, font_id: FontId) -> bool { self.postscript_names_by_font_id .get(&font_id) - .is_some_and(|postscript_name| { + .map_or(false, |postscript_name| { postscript_name == "AppleColorEmoji" || postscript_name == ".AppleColorEmojiUI" }) } diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 4425d4fe24..aedf131909 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -653,7 +653,7 @@ impl MacWindow { .and_then(|titlebar| titlebar.traffic_light_position), transparent_titlebar: titlebar .as_ref() - .is_none_or(|titlebar| titlebar.appears_transparent), + .map_or(true, |titlebar| titlebar.appears_transparent), previous_modifiers_changed_event: None, keystroke_for_do_command: None, do_command_handled: None, @@ -688,7 +688,7 @@ impl MacWindow { }); } - if titlebar.is_none_or(|titlebar| titlebar.appears_transparent) { + if titlebar.map_or(true, |titlebar| titlebar.appears_transparent) { native_window.setTitlebarAppearsTransparent_(YES); native_window.setTitleVisibility_(NSWindowTitleVisibility::NSWindowTitleHidden); } @@ -1090,7 +1090,7 @@ impl PlatformWindow for MacWindow { NSView::removeFromSuperview(blur_view); this.blurred_view = None; } - } else if this.blurred_view.is_none() { + } else if this.blurred_view == 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]; @@ -1478,18 +1478,18 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent: return 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); + 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) { return YES; } - NO - }); - if handled == Some(YES) { - return YES; } } @@ -1624,10 +1624,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 { - return; + if prev_modifiers == modifiers && prev_capslock == capslock { + return; + } } lock.previous_modifiers_changed_event = Some(event.clone()); @@ -1949,7 +1949,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 +1973,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 +1995,10 @@ extern "C" fn attributed_substring_for_proposed_range( let mut adjusted: Option> = None; let selected_text = input_handler.text_for_range(range.clone(), &mut adjusted)?; - if let Some(adjusted) = adjusted - && adjusted != range - { - unsafe { (actual_range as *mut NSRange).write(NSRange::from(adjusted)) }; + if let Some(adjusted) = adjusted { + if adjusted != range { + unsafe { (actual_range as *mut NSRange).write(NSRange::from(adjusted)) }; + } } unsafe { let string: id = msg_send![class!(NSAttributedString), alloc]; @@ -2063,8 +2063,8 @@ fn screen_point_to_gpui_point(this: &Object, position: NSPoint) -> Point let frame = get_frame(this); let window_x = position.x - frame.origin.x; let window_y = frame.size.height - (position.y - frame.origin.y); - - point(px(window_x as f32), px(window_y as f32)) + let position = point(px(window_x as f32), px(window_y as f32)); + position } extern "C" fn dragging_entered(this: &Object, _: Sel, dragging_info: id) -> NSDragOperation { @@ -2073,10 +2073,11 @@ 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) { - window_state.lock().external_files_dragged = true; - return NSDragOperationCopy; + if send_new_event(&window_state, event) { + window_state.lock().external_files_dragged = true; + return NSDragOperationCopy; + } } NSDragOperationNone } diff --git a/crates/gpui/src/platform/scap_screen_capture.rs b/crates/gpui/src/platform/scap_screen_capture.rs index d6d19cd810..32041b655f 100644 --- a/crates/gpui/src/platform/scap_screen_capture.rs +++ b/crates/gpui/src/platform/scap_screen_capture.rs @@ -228,7 +228,7 @@ fn run_capture( display, size, })); - if stream_send_result.is_err() { + if let Err(_) = stream_send_result { return; } while !cancel_stream.load(std::sync::atomic::Ordering::SeqCst) { diff --git a/crates/gpui/src/platform/test/dispatcher.rs b/crates/gpui/src/platform/test/dispatcher.rs index 4ce62c4bdc..16edabfa4b 100644 --- a/crates/gpui/src/platform/test/dispatcher.rs +++ b/crates/gpui/src/platform/test/dispatcher.rs @@ -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 - && due_time <= new_now - { - self.state.lock().time = due_time; - continue; + if let Some(due_time) = next_due_time { + if due_time <= new_now { + self.state.lock().time = due_time; + continue; + } } break; } @@ -270,7 +270,9 @@ impl PlatformDispatcher for TestDispatcher { fn dispatch(&self, runnable: Runnable, label: Option) { { let mut state = self.state.lock(); - if label.is_some_and(|label| state.deprioritized_task_labels.contains(&label)) { + if label.map_or(false, |label| { + state.deprioritized_task_labels.contains(&label) + }) { state.deprioritized_background.push(runnable); } else { state.background.push(runnable); diff --git a/crates/gpui/src/platform/test/platform.rs b/crates/gpui/src/platform/test/platform.rs index 15b909199f..a26b65576c 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -1,9 +1,8 @@ use crate::{ AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels, - DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, - PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PromptButton, - ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SourceMetadata, Task, - TestDisplay, TestWindow, WindowAppearance, WindowParams, size, + ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, PlatformKeyboardLayout, + PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, + SourceMetadata, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams, size, }; use anyhow::Result; use collections::VecDeque; @@ -188,24 +187,24 @@ impl TestPlatform { .push_back(TestPrompt { msg: msg.to_string(), detail: detail.map(|s| s.to_string()), - answers, + answers: answers.clone(), tx, }); rx } pub(crate) fn set_active_window(&self, window: Option) { - let executor = self.foreground_executor(); + let executor = self.foreground_executor().clone(); 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() - && Rc::ptr_eq(&previous_window.0, &window.0) - { - return; + if let Some(window) = window.as_ref() { + if Rc::ptr_eq(&previous_window.0, &window.0) { + return; + } } previous_window.simulate_active_status_change(false); } @@ -238,10 +237,6 @@ impl Platform for TestPlatform { Box::new(TestKeyboardLayout) } - fn keyboard_mapper(&self) -> Rc { - Rc::new(DummyKeyboardMapper) - } - fn on_keyboard_layout_change(&self, _: Box) {} fn run(&self, _on_finish_launching: Box) { @@ -341,7 +336,6 @@ impl Platform for TestPlatform { fn prompt_for_new_path( &self, directory: &std::path::Path, - _suggested_name: Option<&str>, ) -> oneshot::Receiver>> { let (tx, rx) = oneshot::channel(); self.background_executor() diff --git a/crates/gpui/src/platform/windows.rs b/crates/gpui/src/platform/windows.rs index 77e0ca41bf..5268d3ccba 100644 --- a/crates/gpui/src/platform/windows.rs +++ b/crates/gpui/src/platform/windows.rs @@ -10,7 +10,6 @@ mod keyboard; mod platform; mod system_settings; mod util; -mod vsync; mod window; mod wrapper; @@ -26,7 +25,6 @@ pub(crate) use keyboard::*; pub(crate) use platform::*; pub(crate) use system_settings::*; pub(crate) use util::*; -pub(crate) use vsync::*; pub(crate) use window::*; pub(crate) use wrapper::*; diff --git a/crates/gpui/src/platform/windows/color_text_raster.hlsl b/crates/gpui/src/platform/windows/color_text_raster.hlsl deleted file mode 100644 index ccc5fa26f0..0000000000 --- a/crates/gpui/src/platform/windows/color_text_raster.hlsl +++ /dev/null @@ -1,39 +0,0 @@ -struct RasterVertexOutput { - float4 position : SV_Position; - float2 texcoord : TEXCOORD0; -}; - -RasterVertexOutput emoji_rasterization_vertex(uint vertexID : SV_VERTEXID) -{ - RasterVertexOutput output; - output.texcoord = float2((vertexID << 1) & 2, vertexID & 2); - output.position = float4(output.texcoord * 2.0f - 1.0f, 0.0f, 1.0f); - output.position.y = -output.position.y; - - return output; -} - -struct PixelInput { - float4 position: SV_Position; - float2 texcoord : TEXCOORD0; -}; - -struct Bounds { - int2 origin; - int2 size; -}; - -Texture2D t_layer : register(t0); -SamplerState s_layer : register(s0); - -cbuffer GlyphLayerTextureParams : register(b0) { - Bounds bounds; - float4 run_color; -}; - -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); -} diff --git a/crates/gpui/src/platform/windows/direct_write.rs b/crates/gpui/src/platform/windows/direct_write.rs index a86a1fab62..ada306c15c 100644 --- a/crates/gpui/src/platform/windows/direct_write.rs +++ b/crates/gpui/src/platform/windows/direct_write.rs @@ -10,11 +10,10 @@ use windows::{ Foundation::*, Globalization::GetUserDefaultLocaleName, Graphics::{ - Direct3D::D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP, - Direct3D11::*, + Direct2D::{Common::*, *}, DirectWrite::*, Dxgi::Common::*, - Gdi::{IsRectEmpty, LOGFONTW}, + Gdi::LOGFONTW, Imaging::*, }, System::SystemServices::LOCALE_NAME_MAX_LENGTH, @@ -41,21 +40,16 @@ struct DirectWriteComponent { locale: String, factory: IDWriteFactory5, bitmap_factory: AgileReference, + d2d1_factory: ID2D1Factory, in_memory_loader: IDWriteInMemoryFontFileLoader, builder: IDWriteFontSetBuilder1, text_renderer: Arc, - - render_params: IDWriteRenderingParams3, - gpu_state: GPUState, + render_context: GlyphRenderContext, } -struct GPUState { - device: ID3D11Device, - device_context: ID3D11DeviceContext, - sampler: [Option; 1], - blend_state: ID3D11BlendState, - vertex_shader: ID3D11VertexShader, - pixel_shader: ID3D11PixelShader, +struct GlyphRenderContext { + params: IDWriteRenderingParams3, + dc_target: ID2D1DeviceContext4, } struct DirectWriteState { @@ -76,11 +70,12 @@ struct FontIdentifier { } impl DirectWriteComponent { - pub fn new(bitmap_factory: &IWICImagingFactory, gpu_context: &DirectXDevices) -> Result { - // todo: ideally this would not be a large unsafe block but smaller isolated ones for easier auditing + pub fn new(bitmap_factory: &IWICImagingFactory) -> Result { unsafe { let factory: IDWriteFactory5 = DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED)?; let bitmap_factory = AgileReference::new(bitmap_factory)?; + let d2d1_factory: ID2D1Factory = + D2D1CreateFactory(D2D1_FACTORY_TYPE_MULTI_THREADED, None)?; // The `IDWriteInMemoryFontFileLoader` here is supported starting from // Windows 10 Creators Update, which consequently requires the entire // `DirectWriteTextSystem` to run on `win10 1703`+. @@ -91,132 +86,60 @@ impl DirectWriteComponent { GetUserDefaultLocaleName(&mut locale_vec); 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 render_context = GlyphRenderContext::new(&factory, &d2d1_factory)?; Ok(DirectWriteComponent { locale, factory, bitmap_factory, + d2d1_factory, in_memory_loader, builder, text_renderer, - render_params, - gpu_state, + render_context, }) } } } -impl GPUState { - fn new(gpu_context: &DirectXDevices) -> Result { - let device = gpu_context.device.clone(); - let device_context = gpu_context.device_context.clone(); +impl GlyphRenderContext { + pub fn new(factory: &IDWriteFactory5, d2d1_factory: &ID2D1Factory) -> Result { + unsafe { + 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(); - let blend_state = { - let mut blend_state = None; - let desc = D3D11_BLEND_DESC { - AlphaToCoverageEnable: false.into(), - IndependentBlendEnable: false.into(), - RenderTarget: [ - D3D11_RENDER_TARGET_BLEND_DESC { - BlendEnable: true.into(), - SrcBlend: D3D11_BLEND_SRC_ALPHA, - DestBlend: D3D11_BLEND_INV_SRC_ALPHA, - BlendOp: D3D11_BLEND_OP_ADD, - SrcBlendAlpha: D3D11_BLEND_SRC_ALPHA, - DestBlendAlpha: D3D11_BLEND_INV_SRC_ALPHA, - BlendOpAlpha: D3D11_BLEND_OP_ADD, - RenderTargetWriteMask: D3D11_COLOR_WRITE_ENABLE_ALL.0 as u8, - }, - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - Default::default(), - ], - }; - unsafe { device.CreateBlendState(&desc, Some(&mut blend_state)) }?; - blend_state.unwrap() - }; - - let sampler = { - let mut sampler = None; - let desc = D3D11_SAMPLER_DESC { - Filter: D3D11_FILTER_MIN_MAG_MIP_POINT, - AddressU: D3D11_TEXTURE_ADDRESS_BORDER, - AddressV: D3D11_TEXTURE_ADDRESS_BORDER, - AddressW: D3D11_TEXTURE_ADDRESS_BORDER, - MipLODBias: 0.0, - MaxAnisotropy: 1, - ComparisonFunc: D3D11_COMPARISON_ALWAYS, - BorderColor: [0.0, 0.0, 0.0, 0.0], - MinLOD: 0.0, - MaxLOD: 0.0, - }; - unsafe { device.CreateSamplerState(&desc, Some(&mut sampler)) }?; - [sampler] - }; - - let vertex_shader = { - let source = shader_resources::RawShaderBytes::new( - shader_resources::ShaderModule::EmojiRasterization, - shader_resources::ShaderTarget::Vertex, + let params = factory.CreateCustomRenderingParams( + gamma, + enhanced_contrast, + gray_contrast, + cleartype_level, + DWRITE_PIXEL_GEOMETRY_RGB, + DWRITE_RENDERING_MODE1_NATURAL_SYMMETRIC, + grid_fit_mode, )?; - let mut shader = None; - unsafe { device.CreateVertexShader(source.as_bytes(), None, Some(&mut shader)) }?; - shader.unwrap() - }; + let dc_target = { + let target = d2d1_factory.CreateDCRenderTarget(&get_render_target_property( + DXGI_FORMAT_B8G8R8A8_UNORM, + D2D1_ALPHA_MODE_PREMULTIPLIED, + ))?; + let target = target.cast::()?; + target.SetTextRenderingParams(¶ms); + target + }; - let pixel_shader = { - let source = shader_resources::RawShaderBytes::new( - shader_resources::ShaderModule::EmojiRasterization, - shader_resources::ShaderTarget::Fragment, - )?; - let mut shader = None; - unsafe { device.CreatePixelShader(source.as_bytes(), None, Some(&mut shader)) }?; - shader.unwrap() - }; - - Ok(Self { - device, - device_context, - sampler, - blend_state, - vertex_shader, - pixel_shader, - }) + Ok(Self { params, dc_target }) + } } } impl DirectWriteTextSystem { - pub(crate) fn new( - gpu_context: &DirectXDevices, - bitmap_factory: &IWICImagingFactory, - ) -> Result { - let components = DirectWriteComponent::new(bitmap_factory, gpu_context)?; + pub(crate) fn new(bitmap_factory: &IWICImagingFactory) -> Result { + let components = DirectWriteComponent::new(bitmap_factory)?; let system_font_collection = unsafe { let mut result = std::mem::zeroed(); components @@ -498,9 +421,8 @@ impl DirectWriteState { ) .unwrap() } else { - let family = self.system_ui_font_name.clone(); self.find_font_id( - font_name_with_fallbacks(target_font.family.as_ref(), family.as_ref()), + target_font.family.as_ref(), target_font.weight, target_font.style, &target_font.features, @@ -513,6 +435,7 @@ impl DirectWriteState { } #[cfg(not(any(test, feature = "test-support")))] { + let family = self.system_ui_font_name.clone(); log::error!("{} not found, use {} instead.", target_font.family, family); self.get_font_id_from_font_collection( family.as_ref(), @@ -725,13 +648,15 @@ impl DirectWriteState { } } - fn create_glyph_run_analysis( - &self, - params: &RenderGlyphParams, - ) -> Result { + fn raster_bounds(&self, params: &RenderGlyphParams) -> Result> { + let render_target = &self.components.render_context.dc_target; + unsafe { + render_target.SetUnitMode(D2D1_UNIT_MODE_DIPS); + render_target.SetDpi(96.0 * params.scale_factor, 96.0 * params.scale_factor); + } let font = &self.fonts[params.font_id.0]; let glyph_id = [params.glyph_id.0 as u16]; - let advance = [0.0]; + let advance = [0.0f32]; let offset = [DWRITE_GLYPH_OFFSET::default()]; let glyph_run = DWRITE_GLYPH_RUN { fontFace: unsafe { std::mem::transmute_copy(&font.font_face) }, @@ -743,87 +668,44 @@ impl DirectWriteState { isSideways: BOOL(0), bidiLevel: 0, }; - let transform = DWRITE_MATRIX { - m11: params.scale_factor, - m12: 0.0, - m21: 0.0, - m22: params.scale_factor, - dx: 0.0, - dy: 0.0, - }; - let subpixel_shift = params - .subpixel_variant - .map(|v| v as f32 / SUBPIXEL_VARIANTS as f32); - let baseline_origin_x = subpixel_shift.x / params.scale_factor; - let baseline_origin_y = subpixel_shift.y / params.scale_factor; - - let mut rendering_mode = DWRITE_RENDERING_MODE1::default(); - let mut grid_fit_mode = DWRITE_GRID_FIT_MODE::default(); - 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, - Some(&transform), - false, - DWRITE_OUTLINE_THRESHOLD_ANTIALIASED, + let bounds = unsafe { + render_target.GetGlyphRunWorldBounds( + Vector2 { X: 0.0, Y: 0.0 }, + &glyph_run, DWRITE_MEASURING_MODE_NATURAL, - &self.components.render_params, - &mut rendering_mode, - &mut grid_fit_mode, - )?; + )? + }; + // todo(windows) + // This is a walkaround, deleted when figured out. + let y_offset; + let extra_height; + if params.is_emoji { + y_offset = 0; + extra_height = 0; + } else { + // make some room for scaler. + y_offset = -1; + extra_height = 2; } - let glyph_analysis = unsafe { - self.components.factory.CreateGlyphRunAnalysis( - &glyph_run, - Some(&transform), - 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, - baseline_origin_x, - baseline_origin_y, - ) - }?; - Ok(glyph_analysis) - } - - fn raster_bounds(&self, params: &RenderGlyphParams) -> Result> { - 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() { + if bounds.right < bounds.left { Ok(Bounds { - origin: point(bounds.left.into(), bounds.top.into()), - size: size( - (bounds.right - bounds.left).into(), - (bounds.bottom - bounds.top).into(), - ), + origin: point(0.into(), 0.into()), + size: size(0.into(), 0.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(), - ), - }) - } + Ok(Bounds { + origin: point( + ((bounds.left * params.scale_factor).ceil() as i32).into(), + ((bounds.top * params.scale_factor).ceil() as i32 + y_offset).into(), + ), + size: size( + (((bounds.right - bounds.left) * params.scale_factor).ceil() as i32).into(), + (((bounds.bottom - bounds.top) * params.scale_factor).ceil() as i32 + + extra_height) + .into(), + ), + }) } } @@ -849,95 +731,7 @@ impl DirectWriteState { anyhow::bail!("glyph bounds are empty"); } - let bitmap_data = if params.is_emoji { - if let Ok(color) = self.rasterize_color(params, glyph_bounds) { - color - } else { - let monochrome = self.rasterize_monochrome(params, glyph_bounds)?; - monochrome - .into_iter() - .flat_map(|pixel| [0, 0, 0, pixel]) - .collect::>() - } - } else { - self.rasterize_monochrome(params, glyph_bounds)? - }; - - Ok((glyph_bounds.size, bitmap_data)) - } - - fn rasterize_monochrome( - &self, - params: &RenderGlyphParams, - glyph_bounds: Bounds, - ) -> Result> { - let mut bitmap_data = - vec![0u8; glyph_bounds.size.width.0 as usize * glyph_bounds.size.height.0 as usize * 3]; - - 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, - &RECT { - left: glyph_bounds.origin.x.0, - top: glyph_bounds.origin.y.0, - right: glyph_bounds.size.width.0 + glyph_bounds.origin.x.0, - bottom: glyph_bounds.size.height.0 + glyph_bounds.origin.y.0, - }, - &mut bitmap_data, - )?; - } - - 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) - } - - fn rasterize_color( - &self, - params: &RenderGlyphParams, - glyph_bounds: Bounds, - ) -> Result> { - let bitmap_size = glyph_bounds.size; - let subpixel_shift = params - .subpixel_variant - .map(|v| v as f32 / SUBPIXEL_VARIANTS as f32); - let baseline_origin_x = subpixel_shift.x / params.scale_factor; - let baseline_origin_y = subpixel_shift.y / params.scale_factor; - - let transform = DWRITE_MATRIX { - m11: params.scale_factor, - m12: 0.0, - m21: 0.0, - m22: params.scale_factor, - dx: 0.0, - dy: 0.0, - }; - - let font = &self.fonts[params.font_id.0]; + let font_info = &self.fonts[params.font_id.0]; let glyph_id = [params.glyph_id.0 as u16]; let advance = [glyph_bounds.size.width.0 as f32]; let offset = [DWRITE_GLYPH_OFFSET { @@ -945,7 +739,7 @@ impl DirectWriteState { ascenderOffset: glyph_bounds.origin.y.0 as f32 / params.scale_factor, }]; let glyph_run = DWRITE_GLYPH_RUN { - fontFace: unsafe { std::mem::transmute_copy(&font.font_face) }, + fontFace: unsafe { std::mem::transmute_copy(&font_info.font_face) }, fontEmSize: params.font_size.0, glyphCount: 1, glyphIndices: glyph_id.as_ptr(), @@ -955,254 +749,160 @@ impl DirectWriteState { bidiLevel: 0, }; - // todo: support formats other than COLR - let color_enumerator = unsafe { - self.components.factory.TranslateColorGlyphRun( - Vector2::new(baseline_origin_x, baseline_origin_y), - &glyph_run, - None, - DWRITE_GLYPH_IMAGE_FORMATS_COLR, - DWRITE_MEASURING_MODE_NATURAL, - Some(&transform), - 0, - ) - }?; + // Add an extra pixel when the subpixel variant isn't zero to make room for anti-aliasing. + let mut bitmap_size = glyph_bounds.size; + if params.subpixel_variant.x > 0 { + bitmap_size.width += DevicePixels(1); + } + if params.subpixel_variant.y > 0 { + bitmap_size.height += DevicePixels(1); + } + let bitmap_size = bitmap_size; - let mut glyph_layers = Vec::new(); - loop { - let color_run = unsafe { color_enumerator.GetCurrentRun() }?; - let color_run = unsafe { &*color_run }; - let image_format = color_run.glyphImageFormat & !DWRITE_GLYPH_IMAGE_FORMATS_TRUETYPE; - if image_format == DWRITE_GLYPH_IMAGE_FORMATS_COLR { - let color_analysis = unsafe { - self.components.factory.CreateGlyphRunAnalysis( - &color_run.Base.glyphRun as *const _, - Some(&transform), - DWRITE_RENDERING_MODE1_NATURAL_SYMMETRIC, - DWRITE_MEASURING_MODE_NATURAL, - DWRITE_GRID_FIT_MODE_DEFAULT, - DWRITE_TEXT_ANTIALIAS_MODE_CLEARTYPE, - baseline_origin_x, - baseline_origin_y, - ) - }?; - - let color_bounds = - unsafe { color_analysis.GetAlphaTextureBounds(DWRITE_TEXTURE_CLEARTYPE_3x1) }?; - - 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]; - unsafe { - color_analysis.CreateAlphaTexture( - DWRITE_TEXTURE_CLEARTYPE_3x1, - &color_bounds, - &mut alpha_data, - ) - }?; - - let run_color = { - let run_color = color_run.Base.runColor; - Rgba { - r: run_color.r, - g: run_color.g, - b: run_color.b, - a: run_color.a, - } - }; - 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::>(); - glyph_layers.push(GlyphLayerTexture::new( - &self.components.gpu_state, - run_color, - bounds, - &alpha_data, - )?); - } - } - - let has_next = unsafe { color_enumerator.MoveNext() } - .map(|e| e.as_bool()) - .unwrap_or(false); - if !has_next { - break; - } + let total_bytes; + let bitmap_format; + let render_target_property; + let bitmap_width; + let bitmap_height; + let bitmap_stride; + let bitmap_dpi; + if params.is_emoji { + total_bytes = bitmap_size.height.0 as usize * bitmap_size.width.0 as usize * 4; + bitmap_format = &GUID_WICPixelFormat32bppPBGRA; + render_target_property = get_render_target_property( + DXGI_FORMAT_B8G8R8A8_UNORM, + D2D1_ALPHA_MODE_PREMULTIPLIED, + ); + bitmap_width = bitmap_size.width.0 as u32; + bitmap_height = bitmap_size.height.0 as u32; + bitmap_stride = bitmap_size.width.0 as u32 * 4; + bitmap_dpi = 96.0; + } else { + total_bytes = bitmap_size.height.0 as usize * bitmap_size.width.0 as usize; + bitmap_format = &GUID_WICPixelFormat8bppAlpha; + render_target_property = + get_render_target_property(DXGI_FORMAT_A8_UNORM, D2D1_ALPHA_MODE_STRAIGHT); + bitmap_width = bitmap_size.width.0 as u32 * 2; + bitmap_height = bitmap_size.height.0 as u32 * 2; + bitmap_stride = bitmap_size.width.0 as u32; + bitmap_dpi = 192.0; } - let gpu_state = &self.components.gpu_state; - let params_buffer = { - let desc = D3D11_BUFFER_DESC { - ByteWidth: std::mem::size_of::() as u32, - Usage: D3D11_USAGE_DYNAMIC, - BindFlags: D3D11_BIND_CONSTANT_BUFFER.0 as u32, - CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32, - MiscFlags: 0, - StructureByteStride: 0, + let bitmap_factory = self.components.bitmap_factory.resolve()?; + unsafe { + let bitmap = bitmap_factory.CreateBitmap( + bitmap_width, + bitmap_height, + bitmap_format, + WICBitmapCacheOnLoad, + )?; + let render_target = self + .components + .d2d1_factory + .CreateWicBitmapRenderTarget(&bitmap, &render_target_property)?; + let brush = render_target.CreateSolidColorBrush(&BRUSH_COLOR, None)?; + let subpixel_shift = params + .subpixel_variant + .map(|v| v as f32 / SUBPIXEL_VARIANTS as f32); + let baseline_origin = Vector2 { + X: subpixel_shift.x / params.scale_factor, + Y: subpixel_shift.y / params.scale_factor, }; - let mut buffer = None; - unsafe { - gpu_state - .device - .CreateBuffer(&desc, None, Some(&mut buffer)) - }?; - [buffer] - }; + // This `cast()` action here should never fail since we are running on Win10+, and + // ID2D1DeviceContext4 requires Win8+ + let render_target = render_target.cast::().unwrap(); + render_target.SetUnitMode(D2D1_UNIT_MODE_DIPS); + render_target.SetDpi( + bitmap_dpi * params.scale_factor, + bitmap_dpi * params.scale_factor, + ); + render_target.SetTextRenderingParams(&self.components.render_context.params); + render_target.BeginDraw(); - let render_target_texture = { - let mut texture = None; - let desc = D3D11_TEXTURE2D_DESC { - Width: bitmap_size.width.0 as u32, - Height: bitmap_size.height.0 as u32, - MipLevels: 1, - ArraySize: 1, - Format: DXGI_FORMAT_B8G8R8A8_UNORM, - SampleDesc: DXGI_SAMPLE_DESC { - Count: 1, - Quality: 0, - }, - Usage: D3D11_USAGE_DEFAULT, - BindFlags: D3D11_BIND_RENDER_TARGET.0 as u32, - CPUAccessFlags: 0, - MiscFlags: 0, - }; - unsafe { - gpu_state - .device - .CreateTexture2D(&desc, None, Some(&mut texture)) - }?; - texture.unwrap() - }; - - let render_target_view = { - let desc = D3D11_RENDER_TARGET_VIEW_DESC { - Format: DXGI_FORMAT_B8G8R8A8_UNORM, - ViewDimension: D3D11_RTV_DIMENSION_TEXTURE2D, - Anonymous: D3D11_RENDER_TARGET_VIEW_DESC_0 { - Texture2D: D3D11_TEX2D_RTV { MipSlice: 0 }, - }, - }; - let mut rtv = None; - unsafe { - gpu_state.device.CreateRenderTargetView( - &render_target_texture, - Some(&desc), - Some(&mut rtv), - ) - }?; - [rtv] - }; - - let staging_texture = { - let mut texture = None; - let desc = D3D11_TEXTURE2D_DESC { - Width: bitmap_size.width.0 as u32, - Height: bitmap_size.height.0 as u32, - MipLevels: 1, - ArraySize: 1, - Format: DXGI_FORMAT_B8G8R8A8_UNORM, - SampleDesc: DXGI_SAMPLE_DESC { - Count: 1, - Quality: 0, - }, - Usage: D3D11_USAGE_STAGING, - BindFlags: 0, - CPUAccessFlags: D3D11_CPU_ACCESS_READ.0 as u32, - MiscFlags: 0, - }; - unsafe { - gpu_state - .device - .CreateTexture2D(&desc, None, Some(&mut texture)) - }?; - texture.unwrap() - }; - - let device_context = &gpu_state.device_context; - unsafe { device_context.IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP) }; - unsafe { device_context.VSSetShader(&gpu_state.vertex_shader, None) }; - unsafe { device_context.PSSetShader(&gpu_state.pixel_shader, None) }; - unsafe { device_context.VSSetConstantBuffers(0, Some(¶ms_buffer)) }; - unsafe { device_context.PSSetConstantBuffers(0, Some(¶ms_buffer)) }; - unsafe { device_context.OMSetRenderTargets(Some(&render_target_view), None) }; - unsafe { device_context.PSSetSamplers(0, Some(&gpu_state.sampler)) }; - unsafe { device_context.OMSetBlendState(&gpu_state.blend_state, None, 0xffffffff) }; - - for layer in glyph_layers { - let params = GlyphLayerTextureParams { - run_color: layer.run_color, - bounds: layer.bounds, - }; - unsafe { - let mut dest = std::mem::zeroed(); - gpu_state.device_context.Map( - params_buffer[0].as_ref().unwrap(), + if params.is_emoji { + // WARN: only DWRITE_GLYPH_IMAGE_FORMATS_COLR has been tested + let enumerator = self.components.factory.TranslateColorGlyphRun( + baseline_origin, + &glyph_run as _, + None, + DWRITE_GLYPH_IMAGE_FORMATS_COLR + | DWRITE_GLYPH_IMAGE_FORMATS_SVG + | DWRITE_GLYPH_IMAGE_FORMATS_PNG + | DWRITE_GLYPH_IMAGE_FORMATS_JPEG + | DWRITE_GLYPH_IMAGE_FORMATS_PREMULTIPLIED_B8G8R8A8, + DWRITE_MEASURING_MODE_NATURAL, + None, 0, - D3D11_MAP_WRITE_DISCARD, - 0, - Some(&mut dest), )?; - std::ptr::copy_nonoverlapping(¶ms as *const _, dest.pData as *mut _, 1); - gpu_state - .device_context - .Unmap(params_buffer[0].as_ref().unwrap(), 0); - }; + while enumerator.MoveNext().is_ok() { + let Ok(color_glyph) = enumerator.GetCurrentRun() else { + break; + }; + let color_glyph = &*color_glyph; + let brush_color = translate_color(&color_glyph.Base.runColor); + brush.SetColor(&brush_color); + match color_glyph.glyphImageFormat { + DWRITE_GLYPH_IMAGE_FORMATS_PNG + | DWRITE_GLYPH_IMAGE_FORMATS_JPEG + | DWRITE_GLYPH_IMAGE_FORMATS_PREMULTIPLIED_B8G8R8A8 => render_target + .DrawColorBitmapGlyphRun( + color_glyph.glyphImageFormat, + baseline_origin, + &color_glyph.Base.glyphRun, + color_glyph.measuringMode, + D2D1_COLOR_BITMAP_GLYPH_SNAP_OPTION_DEFAULT, + ), + DWRITE_GLYPH_IMAGE_FORMATS_SVG => render_target.DrawSvgGlyphRun( + baseline_origin, + &color_glyph.Base.glyphRun, + &brush, + None, + color_glyph.Base.paletteIndex as u32, + color_glyph.measuringMode, + ), + _ => render_target.DrawGlyphRun( + baseline_origin, + &color_glyph.Base.glyphRun, + Some(color_glyph.Base.glyphRunDescription as *const _), + &brush, + color_glyph.measuringMode, + ), + } + } + } else { + render_target.DrawGlyphRun( + baseline_origin, + &glyph_run, + None, + &brush, + DWRITE_MEASURING_MODE_NATURAL, + ); + } + render_target.EndDraw(None, None)?; - let texture = [Some(layer.texture_view)]; - unsafe { device_context.PSSetShaderResources(0, Some(&texture)) }; - - let viewport = [D3D11_VIEWPORT { - TopLeftX: layer.bounds.origin.x as f32, - TopLeftY: layer.bounds.origin.y as f32, - Width: layer.bounds.size.width as f32, - Height: layer.bounds.size.height as f32, - MinDepth: 0.0, - MaxDepth: 1.0, - }]; - unsafe { device_context.RSSetViewports(Some(&viewport)) }; - - unsafe { device_context.Draw(4, 0) }; + let mut raw_data = vec![0u8; total_bytes]; + if params.is_emoji { + bitmap.CopyPixels(std::ptr::null() as _, bitmap_stride, &mut raw_data)?; + // Convert from BGRA with premultiplied alpha to BGRA with straight alpha. + for pixel in raw_data.chunks_exact_mut(4) { + let a = pixel[3] as f32 / 255.; + pixel[0] = (pixel[0] as f32 / a) as u8; + pixel[1] = (pixel[1] as f32 / a) as u8; + pixel[2] = (pixel[2] as f32 / a) as u8; + } + } else { + let scaler = bitmap_factory.CreateBitmapScaler()?; + scaler.Initialize( + &bitmap, + bitmap_size.width.0 as u32, + bitmap_size.height.0 as u32, + WICBitmapInterpolationModeHighQualityCubic, + )?; + scaler.CopyPixels(std::ptr::null() as _, bitmap_stride, &mut raw_data)?; + } + Ok((bitmap_size, raw_data)) } - - unsafe { device_context.CopyResource(&staging_texture, &render_target_texture) }; - - let mapped_data = { - let mut mapped_data = D3D11_MAPPED_SUBRESOURCE::default(); - unsafe { - device_context.Map( - &staging_texture, - 0, - D3D11_MAP_READ, - 0, - Some(&mut mapped_data), - ) - }?; - mapped_data - }; - let mut rasterized = - vec![0u8; (bitmap_size.width.0 as u32 * bitmap_size.height.0 as u32 * 4) as usize]; - - for y in 0..bitmap_size.height.0 as usize { - let width = bitmap_size.width.0 as usize; - unsafe { - std::ptr::copy_nonoverlapping::( - (mapped_data.pData as *const u8).byte_add(mapped_data.RowPitch as usize * y), - rasterized - .as_mut_ptr() - .byte_add(width * y * std::mem::size_of::()), - width * std::mem::size_of::(), - ) - }; - } - - Ok(rasterized) } fn get_typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result> { @@ -1276,84 +976,6 @@ impl Drop for DirectWriteState { } } -struct GlyphLayerTexture { - run_color: Rgba, - bounds: Bounds, - texture_view: ID3D11ShaderResourceView, - // holding on to the texture to not RAII drop it - _texture: ID3D11Texture2D, -} - -impl GlyphLayerTexture { - pub fn new( - gpu_state: &GPUState, - run_color: Rgba, - bounds: Bounds, - alpha_data: &[u8], - ) -> Result { - let texture_size = bounds.size; - - let desc = D3D11_TEXTURE2D_DESC { - Width: texture_size.width as u32, - Height: texture_size.height as u32, - MipLevels: 1, - ArraySize: 1, - Format: DXGI_FORMAT_R8G8B8A8_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, - MiscFlags: 0, - }; - - let texture = { - let mut texture: Option = None; - unsafe { - gpu_state - .device - .CreateTexture2D(&desc, None, Some(&mut texture))? - }; - texture.unwrap() - }; - let texture_view = { - let mut view: Option = None; - unsafe { - gpu_state - .device - .CreateShaderResourceView(&texture, None, Some(&mut view))? - }; - view.unwrap() - }; - - unsafe { - gpu_state.device_context.UpdateSubresource( - &texture, - 0, - None, - alpha_data.as_ptr() as _, - (texture_size.width * 4) as u32, - 0, - ) - }; - - Ok(GlyphLayerTexture { - run_color, - bounds, - texture_view, - _texture: texture, - }) - } -} - -#[repr(C)] -struct GlyphLayerTextureParams { - bounds: Bounds, - run_color: Rgba, -} - struct TextRendererWrapper(pub IDWriteTextRenderer); impl TextRendererWrapper { @@ -1784,7 +1406,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 { @@ -1848,6 +1470,16 @@ fn get_name(string: IDWriteLocalizedStrings, locale: &str) -> Result { Ok(String::from_utf16_lossy(&name_vec[..name_length])) } +#[inline] +fn translate_color(color: &DWRITE_COLOR_F) -> D2D1_COLOR_F { + D2D1_COLOR_F { + r: color.r, + g: color.g, + b: color.b, + a: color.a, + } +} + fn get_system_ui_font_name() -> SharedString { unsafe { let mut info: LOGFONTW = std::mem::zeroed(); @@ -1872,6 +1504,24 @@ fn get_system_ui_font_name() -> SharedString { } } +#[inline] +fn get_render_target_property( + pixel_format: DXGI_FORMAT, + alpha_mode: D2D1_ALPHA_MODE, +) -> D2D1_RENDER_TARGET_PROPERTIES { + D2D1_RENDER_TARGET_PROPERTIES { + r#type: D2D1_RENDER_TARGET_TYPE_DEFAULT, + pixelFormat: D2D1_PIXEL_FORMAT { + format: pixel_format, + alphaMode: alpha_mode, + }, + dpiX: 96.0, + dpiY: 96.0, + usage: D2D1_RENDER_TARGET_USAGE_NONE, + minLevel: D2D1_FEATURE_LEVEL_DEFAULT, + } +} + // One would think that with newer DirectWrite method: IDWriteFontFace4::GetGlyphImageFormats // but that doesn't seem to work for some glyphs, say ❤ fn is_color_glyph( @@ -1911,6 +1561,12 @@ fn is_color_glyph( } const DEFAULT_LOCALE_NAME: PCWSTR = windows::core::w!("en-US"); +const BRUSH_COLOR: D2D1_COLOR_F = D2D1_COLOR_F { + r: 1.0, + g: 1.0, + b: 1.0, + a: 1.0, +}; #[cfg(test)] mod tests { diff --git a/crates/gpui/src/platform/windows/directx_atlas.rs b/crates/gpui/src/platform/windows/directx_atlas.rs index 6bced4c11d..988943c766 100644 --- a/crates/gpui/src/platform/windows/directx_atlas.rs +++ b/crates/gpui/src/platform/windows/directx_atlas.rs @@ -7,7 +7,7 @@ use windows::Win32::Graphics::{ D3D11_USAGE_DEFAULT, ID3D11Device, ID3D11DeviceContext, ID3D11ShaderResourceView, ID3D11Texture2D, }, - Dxgi::Common::*, + Dxgi::Common::{DXGI_FORMAT_A8_UNORM, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_SAMPLE_DESC}, }; use crate::{ @@ -167,7 +167,7 @@ impl DirectXAtlasState { let bytes_per_pixel; match kind { AtlasTextureKind::Monochrome => { - pixel_format = DXGI_FORMAT_R8_UNORM; + pixel_format = DXGI_FORMAT_A8_UNORM; bind_flag = D3D11_BIND_SHADER_RESOURCE; bytes_per_pixel = 1; } diff --git a/crates/gpui/src/platform/windows/directx_renderer.rs b/crates/gpui/src/platform/windows/directx_renderer.rs index f84a1c1b6d..0823bf17e0 100644 --- a/crates/gpui/src/platform/windows/directx_renderer.rs +++ b/crates/gpui/src/platform/windows/directx_renderer.rs @@ -42,8 +42,8 @@ pub(crate) struct DirectXRenderer { pub(crate) struct DirectXDevices { adapter: IDXGIAdapter1, dxgi_factory: IDXGIFactory6, - pub(crate) device: ID3D11Device, - pub(crate) device_context: ID3D11DeviceContext, + device: ID3D11Device, + device_context: ID3D11DeviceContext, dxgi_device: Option, } @@ -187,7 +187,7 @@ impl DirectXRenderer { self.resources.viewport[0].Width, self.resources.viewport[0].Height, ], - _pad: 0, + ..Default::default() }], )?; unsafe { @@ -286,6 +286,7 @@ impl DirectXRenderer { Ok(()) } + #[profiling::function] pub(crate) fn draw(&mut self, scene: &Scene) -> Result<()> { self.pre_draw()?; for batch in scene.batches() { @@ -435,7 +436,7 @@ impl DirectXRenderer { xy_position: v.xy_position, st_position: v.st_position, color: path.color, - bounds: path.clipped_bounds(), + bounds: path.bounds.intersect(&path.content_mask.bounds), })); } @@ -487,13 +488,13 @@ impl DirectXRenderer { paths .iter() .map(|path| PathSprite { - bounds: path.clipped_bounds(), + bounds: path.bounds, }) .collect::>() } else { - let mut bounds = first_path.clipped_bounds(); + let mut bounds = first_path.bounds; for path in paths.iter().skip(1) { - bounds = bounds.union(&path.clipped_bounds()); + bounds = bounds.union(&path.bounds); } vec![PathSprite { bounds }] }; @@ -758,7 +759,7 @@ impl DirectXRenderPipelines { impl DirectComposition { pub fn new(dxgi_device: &IDXGIDevice, hwnd: HWND) -> Result { - 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() }?; @@ -1144,7 +1145,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) = @@ -1441,7 +1442,7 @@ fn report_live_objects(device: &ID3D11Device) -> Result<()> { const BUFFER_COUNT: usize = 3; -pub(crate) mod shader_resources { +mod shader_resources { use anyhow::Result; #[cfg(debug_assertions)] @@ -1454,7 +1455,7 @@ pub(crate) mod shader_resources { }; #[derive(Copy, Clone, Debug, Eq, PartialEq)] - pub(crate) enum ShaderModule { + pub(super) enum ShaderModule { Quad, Shadow, Underline, @@ -1462,16 +1463,15 @@ pub(crate) mod shader_resources { PathSprite, MonochromeSprite, PolychromeSprite, - EmojiRasterization, } #[derive(Copy, Clone, Debug, Eq, PartialEq)] - pub(crate) enum ShaderTarget { + pub(super) enum ShaderTarget { Vertex, Fragment, } - pub(crate) struct RawShaderBytes<'t> { + pub(super) struct RawShaderBytes<'t> { inner: &'t [u8], #[cfg(debug_assertions)] @@ -1479,7 +1479,7 @@ pub(crate) mod shader_resources { } impl<'t> RawShaderBytes<'t> { - pub(crate) fn new(module: ShaderModule, target: ShaderTarget) -> Result { + pub(super) fn new(module: ShaderModule, target: ShaderTarget) -> Result { #[cfg(not(debug_assertions))] { Ok(Self::from_bytes(module, target)) @@ -1497,7 +1497,7 @@ pub(crate) mod shader_resources { } } - pub(crate) fn as_bytes(&'t self) -> &'t [u8] { + pub(super) fn as_bytes(&'t self) -> &'t [u8] { self.inner } @@ -1532,10 +1532,6 @@ pub(crate) mod shader_resources { ShaderTarget::Vertex => POLYCHROME_SPRITE_VERTEX_BYTES, ShaderTarget::Fragment => POLYCHROME_SPRITE_FRAGMENT_BYTES, }, - ShaderModule::EmojiRasterization => match target { - ShaderTarget::Vertex => EMOJI_RASTERIZATION_VERTEX_BYTES, - ShaderTarget::Fragment => EMOJI_RASTERIZATION_FRAGMENT_BYTES, - }, }; Self { inner: bytes } } @@ -1544,12 +1540,6 @@ pub(crate) mod shader_resources { #[cfg(debug_assertions)] pub(super) fn build_shader_blob(entry: ShaderModule, target: ShaderTarget) -> Result { unsafe { - let shader_name = if matches!(entry, ShaderModule::EmojiRasterization) { - "color_text_raster.hlsl" - } else { - "shaders.hlsl" - }; - let entry = format!( "{}_{}\0", entry.as_str(), @@ -1566,7 +1556,7 @@ pub(crate) mod shader_resources { let mut compile_blob = None; let mut error_blob = None; let shader_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join(&format!("src/platform/windows/{}", shader_name)) + .join("src/platform/windows/shaders.hlsl") .canonicalize()?; let entry_point = PCSTR::from_raw(entry.as_ptr()); @@ -1612,7 +1602,6 @@ pub(crate) mod shader_resources { ShaderModule::PathSprite => "path_sprite", ShaderModule::MonochromeSprite => "monochrome_sprite", ShaderModule::PolychromeSprite => "polychrome_sprite", - ShaderModule::EmojiRasterization => "emoji_rasterization", } } } @@ -1624,10 +1613,11 @@ mod nvidia { os::raw::{c_char, c_int, c_uint}, }; - use anyhow::Result; - use windows::{Win32::System::LibraryLoader::GetProcAddress, core::s}; - - use crate::with_dll_library; + use anyhow::{Context, Result}; + use windows::{ + Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryA}, + core::s, + }; // https://github.com/NVIDIA/nvapi/blob/7cb76fce2f52de818b3da497af646af1ec16ce27/nvapi_lite_common.h#L180 const NVAPI_SHORT_STRING_MAX: usize = 64; @@ -1644,12 +1634,13 @@ mod nvidia { ) -> c_int; pub(super) fn get_driver_version() -> Result { - #[cfg(target_pointer_width = "64")] - let nvidia_dll_name = s!("nvapi64.dll"); - #[cfg(target_pointer_width = "32")] - let nvidia_dll_name = s!("nvapi.dll"); + unsafe { + // Try to load the NVIDIA driver DLL + #[cfg(target_pointer_width = "64")] + let nvidia_dll = LoadLibraryA(s!("nvapi64.dll")).context("Can't load nvapi64.dll")?; + #[cfg(target_pointer_width = "32")] + let nvidia_dll = LoadLibraryA(s!("nvapi.dll")).context("Can't load nvapi.dll")?; - with_dll_library(nvidia_dll_name, |nvidia_dll| unsafe { let nvapi_query_addr = GetProcAddress(nvidia_dll, s!("nvapi_QueryInterface")) .ok_or_else(|| anyhow::anyhow!("Failed to get nvapi_QueryInterface address"))?; let nvapi_query: extern "C" fn(u32) -> *mut () = std::mem::transmute(nvapi_query_addr); @@ -1684,17 +1675,18 @@ mod nvidia { minor, branch_string.to_string_lossy() )) - }) + } } } mod amd { use std::os::raw::{c_char, c_int, c_void}; - use anyhow::Result; - use windows::{Win32::System::LibraryLoader::GetProcAddress, core::s}; - - use crate::with_dll_library; + use anyhow::{Context, Result}; + use windows::{ + Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryA}, + core::s, + }; // https://github.com/GPUOpen-LibrariesAndSDKs/AGS_SDK/blob/5d8812d703d0335741b6f7ffc37838eeb8b967f7/ags_lib/inc/amd_ags.h#L145 const AGS_CURRENT_VERSION: i32 = (6 << 22) | (3 << 12); @@ -1728,12 +1720,14 @@ mod amd { type agsDeInitialize_t = unsafe extern "C" fn(context: *mut AGSContext) -> c_int; pub(super) fn get_driver_version() -> Result { - #[cfg(target_pointer_width = "64")] - let amd_dll_name = s!("amd_ags_x64.dll"); - #[cfg(target_pointer_width = "32")] - let amd_dll_name = s!("amd_ags_x86.dll"); + unsafe { + #[cfg(target_pointer_width = "64")] + let amd_dll = + LoadLibraryA(s!("amd_ags_x64.dll")).context("Failed to load AMD AGS library")?; + #[cfg(target_pointer_width = "32")] + let amd_dll = + LoadLibraryA(s!("amd_ags_x86.dll")).context("Failed to load AMD AGS library")?; - with_dll_library(amd_dll_name, |amd_dll| unsafe { let ags_initialize_addr = GetProcAddress(amd_dll, s!("agsInitialize")) .ok_or_else(|| anyhow::anyhow!("Failed to get agsInitialize address"))?; let ags_deinitialize_addr = GetProcAddress(amd_dll, s!("agsDeInitialize")) @@ -1779,7 +1773,7 @@ mod amd { ags_deinitialize(context); Ok(format!("{} ({})", software_version, driver_version)) - }) + } } } diff --git a/crates/gpui/src/platform/windows/dispatcher.rs b/crates/gpui/src/platform/windows/dispatcher.rs index f554dea128..e5b9c020d5 100644 --- a/crates/gpui/src/platform/windows/dispatcher.rs +++ b/crates/gpui/src/platform/windows/dispatcher.rs @@ -9,8 +9,10 @@ use parking::Parker; use parking_lot::Mutex; use util::ResultExt; use windows::{ + Foundation::TimeSpan, System::Threading::{ - ThreadPool, ThreadPoolTimer, TimerElapsedHandler, WorkItemHandler, WorkItemPriority, + ThreadPool, ThreadPoolTimer, TimerElapsedHandler, WorkItemHandler, WorkItemOptions, + WorkItemPriority, }, Win32::{ Foundation::{LPARAM, WPARAM}, @@ -54,7 +56,12 @@ impl WindowsDispatcher { Ok(()) }) }; - ThreadPool::RunWithPriorityAsync(&handler, WorkItemPriority::High).log_err(); + ThreadPool::RunWithPriorityAndOptionsAsync( + &handler, + WorkItemPriority::High, + WorkItemOptions::TimeSliced, + ) + .log_err(); } fn dispatch_on_threadpool_after(&self, runnable: Runnable, duration: Duration) { @@ -65,7 +72,12 @@ impl WindowsDispatcher { Ok(()) }) }; - ThreadPoolTimer::CreateTimer(&handler, duration.into()).log_err(); + 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(); } } diff --git a/crates/gpui/src/platform/windows/events.rs b/crates/gpui/src/platform/windows/events.rs index 4def6a11a5..ad211b827f 100644 --- a/crates/gpui/src/platform/windows/events.rs +++ b/crates/gpui/src/platform/windows/events.rs @@ -28,685 +28,716 @@ pub(crate) const WM_GPUI_FORCE_UPDATE_WINDOW: u32 = WM_USER + 5; const SIZE_MOVE_LOOP_TIMER_ID: usize = 1; const AUTO_HIDE_TASKBAR_THICKNESS_PX: i32 = 1; -impl WindowsWindowInner { - pub(crate) fn handle_msg( - self: &Rc, - handle: HWND, - msg: u32, - wparam: WPARAM, - lparam: LPARAM, - ) -> LRESULT { - 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), - WM_ENTERSIZEMOVE | WM_ENTERMENULOOP => self.handle_size_move_loop(handle), - WM_EXITSIZEMOVE | WM_EXITMENULOOP => self.handle_size_move_loop_exit(handle), - WM_TIMER => self.handle_timer_msg(handle, wparam), - WM_NCCALCSIZE => self.handle_calc_client_size(handle, wparam, lparam), - WM_DPICHANGED => self.handle_dpi_changed_msg(handle, wparam, lparam), - WM_DISPLAYCHANGE => self.handle_display_change_msg(handle), - WM_NCHITTEST => self.handle_hit_test_msg(handle, msg, wparam, lparam), - WM_PAINT => self.handle_paint_msg(handle), - WM_CLOSE => self.handle_close_msg(), - WM_DESTROY => self.handle_destroy_msg(handle), - WM_MOUSEMOVE => self.handle_mouse_move_msg(handle, lparam, wparam), - WM_MOUSELEAVE | WM_NCMOUSELEAVE => self.handle_mouse_leave_msg(), - WM_NCMOUSEMOVE => self.handle_nc_mouse_move_msg(handle, lparam), - WM_NCLBUTTONDOWN => { - self.handle_nc_mouse_down_msg(handle, MouseButton::Left, wparam, lparam) - } - WM_NCRBUTTONDOWN => { - self.handle_nc_mouse_down_msg(handle, MouseButton::Right, wparam, lparam) - } - WM_NCMBUTTONDOWN => { - self.handle_nc_mouse_down_msg(handle, MouseButton::Middle, wparam, lparam) - } - WM_NCLBUTTONUP => { - self.handle_nc_mouse_up_msg(handle, MouseButton::Left, wparam, lparam) - } - WM_NCRBUTTONUP => { - self.handle_nc_mouse_up_msg(handle, MouseButton::Right, wparam, lparam) - } - WM_NCMBUTTONUP => { - self.handle_nc_mouse_up_msg(handle, MouseButton::Middle, wparam, lparam) - } - WM_LBUTTONDOWN => self.handle_mouse_down_msg(handle, MouseButton::Left, lparam), - WM_RBUTTONDOWN => self.handle_mouse_down_msg(handle, MouseButton::Right, lparam), - WM_MBUTTONDOWN => self.handle_mouse_down_msg(handle, MouseButton::Middle, lparam), - WM_XBUTTONDOWN => { - self.handle_xbutton_msg(handle, wparam, lparam, Self::handle_mouse_down_msg) - } - WM_LBUTTONUP => self.handle_mouse_up_msg(handle, MouseButton::Left, lparam), - WM_RBUTTONUP => self.handle_mouse_up_msg(handle, MouseButton::Right, lparam), - WM_MBUTTONUP => self.handle_mouse_up_msg(handle, MouseButton::Middle, lparam), - WM_XBUTTONUP => { - self.handle_xbutton_msg(handle, wparam, lparam, Self::handle_mouse_up_msg) - } - WM_MOUSEWHEEL => self.handle_mouse_wheel_msg(handle, wparam, lparam), - WM_MOUSEHWHEEL => self.handle_mouse_horizontal_wheel_msg(handle, wparam, lparam), - WM_SYSKEYDOWN => self.handle_syskeydown_msg(handle, wparam, lparam), - WM_SYSKEYUP => self.handle_syskeyup_msg(handle, wparam, lparam), - WM_SYSCOMMAND => self.handle_system_command(wparam), - WM_KEYDOWN => self.handle_keydown_msg(handle, wparam, lparam), - WM_KEYUP => self.handle_keyup_msg(handle, wparam, lparam), - WM_CHAR => self.handle_char_msg(wparam), - WM_DEADCHAR => self.handle_dead_char_msg(wparam), - WM_IME_STARTCOMPOSITION => self.handle_ime_position(handle), - 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_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), - _ => None, - }; - if let Some(n) = handled { - LRESULT(n) - } else { - unsafe { DefWindowProcW(handle, msg, wparam, lparam) } +pub(crate) fn handle_msg( + handle: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, + state_ptr: Rc, +) -> LRESULT { + let handled = match msg { + WM_ACTIVATE => handle_activate_msg(wparam, state_ptr), + WM_CREATE => handle_create_msg(handle, state_ptr), + WM_DEVICECHANGE => handle_device_change_msg(handle, wparam, state_ptr), + WM_MOVE => handle_move_msg(handle, lparam, state_ptr), + WM_SIZE => handle_size_msg(wparam, lparam, state_ptr), + WM_GETMINMAXINFO => handle_get_min_max_info_msg(lparam, state_ptr), + WM_ENTERSIZEMOVE | WM_ENTERMENULOOP => handle_size_move_loop(handle), + WM_EXITSIZEMOVE | WM_EXITMENULOOP => handle_size_move_loop_exit(handle), + WM_TIMER => handle_timer_msg(handle, wparam, state_ptr), + WM_NCCALCSIZE => handle_calc_client_size(handle, wparam, lparam, state_ptr), + WM_DPICHANGED => handle_dpi_changed_msg(handle, wparam, lparam, state_ptr), + WM_DISPLAYCHANGE => handle_display_change_msg(handle, state_ptr), + WM_NCHITTEST => handle_hit_test_msg(handle, msg, wparam, lparam, state_ptr), + WM_PAINT => handle_paint_msg(handle, state_ptr), + WM_CLOSE => handle_close_msg(state_ptr), + WM_DESTROY => handle_destroy_msg(handle, state_ptr), + WM_MOUSEMOVE => handle_mouse_move_msg(handle, lparam, wparam, state_ptr), + WM_MOUSELEAVE | WM_NCMOUSELEAVE => handle_mouse_leave_msg(state_ptr), + WM_NCMOUSEMOVE => handle_nc_mouse_move_msg(handle, lparam, state_ptr), + WM_NCLBUTTONDOWN => { + handle_nc_mouse_down_msg(handle, MouseButton::Left, wparam, lparam, state_ptr) + } + WM_NCRBUTTONDOWN => { + handle_nc_mouse_down_msg(handle, MouseButton::Right, wparam, lparam, state_ptr) + } + WM_NCMBUTTONDOWN => { + handle_nc_mouse_down_msg(handle, MouseButton::Middle, wparam, lparam, state_ptr) + } + WM_NCLBUTTONUP => { + handle_nc_mouse_up_msg(handle, MouseButton::Left, wparam, lparam, state_ptr) + } + WM_NCRBUTTONUP => { + handle_nc_mouse_up_msg(handle, MouseButton::Right, wparam, lparam, state_ptr) + } + WM_NCMBUTTONUP => { + handle_nc_mouse_up_msg(handle, MouseButton::Middle, wparam, lparam, state_ptr) + } + WM_LBUTTONDOWN => handle_mouse_down_msg(handle, MouseButton::Left, lparam, state_ptr), + WM_RBUTTONDOWN => handle_mouse_down_msg(handle, MouseButton::Right, lparam, state_ptr), + WM_MBUTTONDOWN => handle_mouse_down_msg(handle, MouseButton::Middle, lparam, state_ptr), + WM_XBUTTONDOWN => { + handle_xbutton_msg(handle, wparam, lparam, handle_mouse_down_msg, state_ptr) + } + WM_LBUTTONUP => handle_mouse_up_msg(handle, MouseButton::Left, lparam, state_ptr), + WM_RBUTTONUP => handle_mouse_up_msg(handle, MouseButton::Right, lparam, state_ptr), + WM_MBUTTONUP => handle_mouse_up_msg(handle, MouseButton::Middle, lparam, state_ptr), + WM_XBUTTONUP => handle_xbutton_msg(handle, wparam, lparam, handle_mouse_up_msg, state_ptr), + WM_MOUSEWHEEL => handle_mouse_wheel_msg(handle, wparam, lparam, state_ptr), + WM_MOUSEHWHEEL => handle_mouse_horizontal_wheel_msg(handle, wparam, lparam, state_ptr), + WM_SYSKEYDOWN => handle_syskeydown_msg(handle, wparam, lparam, state_ptr), + WM_SYSKEYUP => handle_syskeyup_msg(handle, wparam, lparam, state_ptr), + WM_SYSCOMMAND => handle_system_command(wparam, state_ptr), + WM_KEYDOWN => handle_keydown_msg(handle, wparam, lparam, state_ptr), + WM_KEYUP => handle_keyup_msg(handle, wparam, lparam, state_ptr), + WM_CHAR => handle_char_msg(wparam, state_ptr), + WM_DEADCHAR => handle_dead_char_msg(wparam, state_ptr), + WM_IME_STARTCOMPOSITION => handle_ime_position(handle, state_ptr), + WM_IME_COMPOSITION => handle_ime_composition(handle, lparam, state_ptr), + WM_SETCURSOR => handle_set_cursor(handle, lparam, state_ptr), + WM_SETTINGCHANGE => handle_system_settings_changed(handle, wparam, lparam, state_ptr), + WM_INPUTLANGCHANGE => handle_input_language_changed(lparam, state_ptr), + WM_GPUI_CURSOR_STYLE_CHANGED => handle_cursor_changed(lparam, state_ptr), + WM_GPUI_FORCE_UPDATE_WINDOW => draw_window(handle, true, state_ptr), + _ => None, + }; + if let Some(n) = handled { + LRESULT(n) + } else { + unsafe { DefWindowProcW(handle, msg, wparam, lparam) } + } +} + +fn handle_move_msg( + handle: HWND, + lparam: LPARAM, + state_ptr: Rc, +) -> Option { + let mut lock = state_ptr.state.borrow_mut(); + let origin = logical_point( + lparam.signed_loword() as f32, + lparam.signed_hiword() as f32, + lock.scale_factor, + ); + lock.origin = origin; + let size = lock.logical_size; + let center_x = origin.x.0 + size.width.0 / 2.; + let center_y = origin.y.0 + size.height.0 / 2.; + let monitor_bounds = lock.display.bounds(); + if center_x < monitor_bounds.left().0 + || center_x > monitor_bounds.right().0 + || center_y < monitor_bounds.top().0 + || center_y > monitor_bounds.bottom().0 + { + // center of the window may have moved to another monitor + let monitor = unsafe { MonitorFromWindow(handle, MONITOR_DEFAULTTONULL) }; + // minimize the window can trigger this event too, in this case, + // monitor is invalid, we do nothing. + if !monitor.is_invalid() && lock.display.handle != monitor { + // we will get the same monitor if we only have one + lock.display = WindowsDisplay::new_with_handle(monitor); } } + if let Some(mut callback) = lock.callbacks.moved.take() { + drop(lock); + callback(); + state_ptr.state.borrow_mut().callbacks.moved = Some(callback); + } + Some(0) +} - fn handle_move_msg(&self, handle: HWND, lparam: LPARAM) -> Option { - let mut lock = self.state.borrow_mut(); - let origin = logical_point( - lparam.signed_loword() as f32, - lparam.signed_hiword() as f32, - lock.scale_factor, +fn handle_get_min_max_info_msg( + lparam: LPARAM, + state_ptr: Rc, +) -> Option { + let lock = state_ptr.state.borrow(); + let min_size = lock.min_size?; + let scale_factor = lock.scale_factor; + let boarder_offset = lock.border_offset; + drop(lock); + unsafe { + let minmax_info = &mut *(lparam.0 as *mut MINMAXINFO); + minmax_info.ptMinTrackSize.x = + min_size.width.scale(scale_factor).0 as i32 + boarder_offset.width_offset; + minmax_info.ptMinTrackSize.y = + min_size.height.scale(scale_factor).0 as i32 + boarder_offset.height_offset; + } + Some(0) +} + +fn handle_size_msg( + wparam: WPARAM, + lparam: LPARAM, + state_ptr: Rc, +) -> Option { + let mut lock = state_ptr.state.borrow_mut(); + + // Don't resize the renderer when the window is minimized, but record that it was minimized so + // that on restore the swap chain can be recreated via `update_drawable_size_even_if_unchanged`. + if wparam.0 == SIZE_MINIMIZED as usize { + lock.restore_from_minimized = lock.callbacks.request_frame.take(); + return Some(0); + } + + let width = lparam.loword().max(1) as i32; + let height = lparam.hiword().max(1) as i32; + let new_size = size(DevicePixels(width), DevicePixels(height)); + let scale_factor = lock.scale_factor; + if lock.restore_from_minimized.is_some() { + lock.callbacks.request_frame = lock.restore_from_minimized.take(); + } else { + lock.renderer.resize(new_size).log_err(); + } + let new_size = new_size.to_pixels(scale_factor); + lock.logical_size = new_size; + if let Some(mut callback) = lock.callbacks.resize.take() { + drop(lock); + callback(new_size, scale_factor); + state_ptr.state.borrow_mut().callbacks.resize = Some(callback); + } + Some(0) +} + +fn handle_size_move_loop(handle: HWND) -> Option { + unsafe { + let ret = SetTimer( + Some(handle), + SIZE_MOVE_LOOP_TIMER_ID, + USER_TIMER_MINIMUM, + None, ); - lock.origin = origin; - let size = lock.logical_size; - let center_x = origin.x.0 + size.width.0 / 2.; - let center_y = origin.y.0 + size.height.0 / 2.; - let monitor_bounds = lock.display.bounds(); - if center_x < monitor_bounds.left().0 - || center_x > monitor_bounds.right().0 - || center_y < monitor_bounds.top().0 - || center_y > monitor_bounds.bottom().0 - { - // center of the window may have moved to another monitor - let monitor = unsafe { MonitorFromWindow(handle, MONITOR_DEFAULTTONULL) }; - // minimize the window can trigger this event too, in this case, - // monitor is invalid, we do nothing. - if !monitor.is_invalid() && lock.display.handle != monitor { - // we will get the same monitor if we only have one - lock.display = WindowsDisplay::new_with_handle(monitor); - } - } - if let Some(mut callback) = lock.callbacks.moved.take() { - drop(lock); - callback(); - self.state.borrow_mut().callbacks.moved = Some(callback); - } - Some(0) - } - - fn handle_get_min_max_info_msg(&self, lparam: LPARAM) -> Option { - let lock = self.state.borrow(); - let min_size = lock.min_size?; - let scale_factor = lock.scale_factor; - let boarder_offset = lock.border_offset; - drop(lock); - unsafe { - let minmax_info = &mut *(lparam.0 as *mut MINMAXINFO); - minmax_info.ptMinTrackSize.x = - min_size.width.scale(scale_factor).0 as i32 + boarder_offset.width_offset; - minmax_info.ptMinTrackSize.y = - min_size.height.scale(scale_factor).0 as i32 + boarder_offset.height_offset; - } - Some(0) - } - - fn handle_size_msg(&self, wparam: WPARAM, lparam: LPARAM) -> Option { - let mut lock = self.state.borrow_mut(); - - // Don't resize the renderer when the window is minimized, but record that it was minimized so - // that on restore the swap chain can be recreated via `update_drawable_size_even_if_unchanged`. - if wparam.0 == SIZE_MINIMIZED as usize { - lock.restore_from_minimized = lock.callbacks.request_frame.take(); - return Some(0); - } - - let width = lparam.loword().max(1) as i32; - let height = lparam.hiword().max(1) as i32; - let new_size = size(DevicePixels(width), DevicePixels(height)); - - let scale_factor = lock.scale_factor; - let mut should_resize_renderer = false; - if lock.restore_from_minimized.is_some() { - lock.callbacks.request_frame = lock.restore_from_minimized.take(); - } else { - should_resize_renderer = true; - } - drop(lock); - - self.handle_size_change(new_size, scale_factor, should_resize_renderer); - Some(0) - } - - fn handle_size_change( - &self, - device_size: Size, - scale_factor: f32, - should_resize_renderer: bool, - ) { - let new_logical_size = device_size.to_pixels(scale_factor); - let mut lock = self.state.borrow_mut(); - lock.logical_size = new_logical_size; - if should_resize_renderer { - lock.renderer.resize(device_size).log_err(); - } - if let Some(mut callback) = lock.callbacks.resize.take() { - drop(lock); - callback(new_logical_size, scale_factor); - self.state.borrow_mut().callbacks.resize = Some(callback); - } - } - - fn handle_size_move_loop(&self, handle: HWND) -> Option { - unsafe { - let ret = SetTimer( - Some(handle), - SIZE_MOVE_LOOP_TIMER_ID, - USER_TIMER_MINIMUM, - None, + if ret == 0 { + log::error!( + "unable to create timer: {}", + std::io::Error::last_os_error() ); - if ret == 0 { - log::error!( - "unable to create timer: {}", - std::io::Error::last_os_error() - ); - } } + } + None +} + +fn handle_size_move_loop_exit(handle: HWND) -> Option { + unsafe { + KillTimer(Some(handle), SIZE_MOVE_LOOP_TIMER_ID).log_err(); + } + None +} + +fn handle_timer_msg( + handle: HWND, + wparam: WPARAM, + state_ptr: Rc, +) -> Option { + if wparam.0 == SIZE_MOVE_LOOP_TIMER_ID { + for runnable in state_ptr.main_receiver.drain() { + runnable.run(); + } + handle_paint_msg(handle, state_ptr) + } else { None } +} - fn handle_size_move_loop_exit(&self, handle: HWND) -> Option { - unsafe { - KillTimer(Some(handle), SIZE_MOVE_LOOP_TIMER_ID).log_err(); +#[profiling::function] +fn handle_paint_msg(handle: HWND, state_ptr: Rc) -> Option { + draw_window(handle, false, state_ptr) +} + +fn handle_close_msg(state_ptr: Rc) -> Option { + let mut callback = state_ptr.state.borrow_mut().callbacks.should_close.take()?; + let should_close = callback(); + state_ptr.state.borrow_mut().callbacks.should_close = Some(callback); + if should_close { None } else { Some(0) } +} + +fn handle_destroy_msg(handle: HWND, state_ptr: Rc) -> Option { + let callback = { + let mut lock = state_ptr.state.borrow_mut(); + lock.callbacks.close.take() + }; + if let Some(callback) = callback { + callback(); + } + unsafe { + PostThreadMessageW( + state_ptr.main_thread_id_win32, + WM_GPUI_CLOSE_ONE_WINDOW, + WPARAM(state_ptr.validation_number), + LPARAM(handle.0 as isize), + ) + .log_err(); + } + Some(0) +} + +fn handle_mouse_move_msg( + handle: HWND, + lparam: LPARAM, + wparam: WPARAM, + state_ptr: Rc, +) -> Option { + start_tracking_mouse(handle, &state_ptr, TME_LEAVE); + + let mut lock = state_ptr.state.borrow_mut(); + let Some(mut func) = lock.callbacks.input.take() else { + return Some(1); + }; + let scale_factor = lock.scale_factor; + drop(lock); + + let pressed_button = match MODIFIERKEYS_FLAGS(wparam.loword() as u32) { + flags if flags.contains(MK_LBUTTON) => Some(MouseButton::Left), + flags if flags.contains(MK_RBUTTON) => Some(MouseButton::Right), + flags if flags.contains(MK_MBUTTON) => Some(MouseButton::Middle), + flags if flags.contains(MK_XBUTTON1) => { + Some(MouseButton::Navigate(NavigationDirection::Back)) } + flags if flags.contains(MK_XBUTTON2) => { + Some(MouseButton::Navigate(NavigationDirection::Forward)) + } + _ => None, + }; + let x = lparam.signed_loword() as f32; + let y = lparam.signed_hiword() as f32; + let input = PlatformInput::MouseMove(MouseMoveEvent { + position: logical_point(x, y, scale_factor), + pressed_button, + modifiers: current_modifiers(), + }); + let handled = !func(input).propagate; + state_ptr.state.borrow_mut().callbacks.input = Some(func); + + if handled { Some(0) } else { Some(1) } +} + +fn handle_mouse_leave_msg(state_ptr: Rc) -> Option { + let mut lock = state_ptr.state.borrow_mut(); + lock.hovered = false; + if let Some(mut callback) = lock.callbacks.hovered_status_change.take() { + drop(lock); + callback(false); + state_ptr.state.borrow_mut().callbacks.hovered_status_change = Some(callback); + } + + Some(0) +} + +fn handle_syskeydown_msg( + handle: HWND, + wparam: WPARAM, + lparam: LPARAM, + state_ptr: Rc, +) -> Option { + let mut lock = state_ptr.state.borrow_mut(); + let input = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| { + PlatformInput::KeyDown(KeyDownEvent { + keystroke, + is_held: lparam.0 & (0x1 << 30) > 0, + }) + })?; + let mut func = lock.callbacks.input.take()?; + drop(lock); + + let handled = !func(input).propagate; + + let mut lock = state_ptr.state.borrow_mut(); + lock.callbacks.input = Some(func); + + if handled { + lock.system_key_handled = true; + Some(0) + } else { + // we need to call `DefWindowProcW`, or we will lose the system-wide `Alt+F4`, `Alt+{other keys}` + // shortcuts. None } +} - fn handle_timer_msg(&self, handle: HWND, wparam: WPARAM) -> Option { - if wparam.0 == SIZE_MOVE_LOOP_TIMER_ID { - for runnable in self.main_receiver.drain() { - runnable.run(); - } - self.handle_paint_msg(handle) - } else { - None - } +fn handle_syskeyup_msg( + handle: HWND, + wparam: WPARAM, + lparam: LPARAM, + state_ptr: Rc, +) -> Option { + let mut lock = state_ptr.state.borrow_mut(); + let input = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| { + PlatformInput::KeyUp(KeyUpEvent { keystroke }) + })?; + let mut func = lock.callbacks.input.take()?; + drop(lock); + func(input); + state_ptr.state.borrow_mut().callbacks.input = Some(func); + + // Always return 0 to indicate that the message was handled, so we could properly handle `ModifiersChanged` event. + Some(0) +} + +// It's a known bug that you can't trigger `ctrl-shift-0`. See: +// https://superuser.com/questions/1455762/ctrl-shift-number-key-combination-has-stopped-working-for-a-few-numbers +#[profiling::function] +fn handle_keydown_msg( + handle: HWND, + wparam: WPARAM, + lparam: LPARAM, + state_ptr: Rc, +) -> Option { + let mut lock = state_ptr.state.borrow_mut(); + lock.keydown_time = Some(std::time::Instant::now()); + println!("WM_KEYDOWN"); + let Some(input) = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| { + PlatformInput::KeyDown(KeyDownEvent { + keystroke, + is_held: lparam.0 & (0x1 << 30) > 0, + }) + }) else { + return Some(1); + }; + drop(lock); + + let is_composing = with_input_handler(&state_ptr, |input_handler| { + input_handler.marked_text_range() + }) + .flatten() + .is_some(); + if is_composing { + translate_message(handle, wparam, lparam); + return Some(0); } - fn handle_paint_msg(&self, handle: HWND) -> Option { - self.draw_window(handle, false) - } + let Some(mut func) = state_ptr.state.borrow_mut().callbacks.input.take() else { + return Some(1); + }; - fn handle_close_msg(&self) -> Option { - let mut callback = self.state.borrow_mut().callbacks.should_close.take()?; - let should_close = callback(); - self.state.borrow_mut().callbacks.should_close = Some(callback); - if should_close { None } else { Some(0) } - } + let handled = !func(input).propagate; - fn handle_destroy_msg(&self, handle: HWND) -> Option { - let callback = { - let mut lock = self.state.borrow_mut(); - lock.callbacks.close.take() - }; - if let Some(callback) = callback { - callback(); - } - unsafe { - PostThreadMessageW( - self.main_thread_id_win32, - WM_GPUI_CLOSE_ONE_WINDOW, - WPARAM(self.validation_number), - LPARAM(handle.0 as isize), - ) - .log_err(); - } + state_ptr.state.borrow_mut().callbacks.input = Some(func); + + if handled { Some(0) + } else { + translate_message(handle, wparam, lparam); + Some(1) } +} - fn handle_mouse_move_msg(&self, handle: HWND, lparam: LPARAM, wparam: WPARAM) -> Option { - self.start_tracking_mouse(handle, TME_LEAVE); +fn handle_keyup_msg( + handle: HWND, + wparam: WPARAM, + lparam: LPARAM, + state_ptr: Rc, +) -> Option { + let mut lock = state_ptr.state.borrow_mut(); + let Some(input) = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| { + PlatformInput::KeyUp(KeyUpEvent { keystroke }) + }) else { + return Some(1); + }; - let mut lock = self.state.borrow_mut(); - let Some(mut func) = lock.callbacks.input.take() else { - return Some(1); - }; - let scale_factor = lock.scale_factor; - drop(lock); + let Some(mut func) = lock.callbacks.input.take() else { + return Some(1); + }; + drop(lock); - let pressed_button = match MODIFIERKEYS_FLAGS(wparam.loword() as u32) { - flags if flags.contains(MK_LBUTTON) => Some(MouseButton::Left), - flags if flags.contains(MK_RBUTTON) => Some(MouseButton::Right), - flags if flags.contains(MK_MBUTTON) => Some(MouseButton::Middle), - flags if flags.contains(MK_XBUTTON1) => { - Some(MouseButton::Navigate(NavigationDirection::Back)) - } - flags if flags.contains(MK_XBUTTON2) => { - Some(MouseButton::Navigate(NavigationDirection::Forward)) - } - _ => None, - }; - let x = lparam.signed_loword() as f32; - let y = lparam.signed_hiword() as f32; - let input = PlatformInput::MouseMove(MouseMoveEvent { - position: logical_point(x, y, scale_factor), - pressed_button, - modifiers: current_modifiers(), - }); - let handled = !func(input).propagate; - self.state.borrow_mut().callbacks.input = Some(func); + let handled = !func(input).propagate; + state_ptr.state.borrow_mut().callbacks.input = Some(func); - if handled { Some(0) } else { Some(1) } - } + if handled { Some(0) } else { Some(1) } +} - fn handle_mouse_leave_msg(&self) -> Option { - let mut lock = self.state.borrow_mut(); - lock.hovered = false; - if let Some(mut callback) = lock.callbacks.hovered_status_change.take() { - drop(lock); - callback(false); - self.state.borrow_mut().callbacks.hovered_status_change = Some(callback); - } +fn handle_char_msg(wparam: WPARAM, state_ptr: Rc) -> Option { + let input = parse_char_message(wparam, &state_ptr)?; + with_input_handler(&state_ptr, |input_handler| { + input_handler.replace_text_in_range(None, &input); + }); - Some(0) - } + Some(0) +} - fn handle_syskeydown_msg(&self, handle: HWND, wparam: WPARAM, lparam: LPARAM) -> Option { - let mut lock = self.state.borrow_mut(); - let input = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| { - PlatformInput::KeyDown(KeyDownEvent { - keystroke, - is_held: lparam.0 & (0x1 << 30) > 0, - }) - })?; - let mut func = lock.callbacks.input.take()?; - drop(lock); +fn handle_dead_char_msg(wparam: WPARAM, state_ptr: Rc) -> Option { + let ch = char::from_u32(wparam.0 as u32)?.to_string(); + with_input_handler(&state_ptr, |input_handler| { + input_handler.replace_and_mark_text_in_range(None, &ch, None); + }); + None +} - let handled = !func(input).propagate; +fn handle_mouse_down_msg( + handle: HWND, + button: MouseButton, + lparam: LPARAM, + state_ptr: Rc, +) -> Option { + unsafe { SetCapture(handle) }; + let mut lock = state_ptr.state.borrow_mut(); + let Some(mut func) = lock.callbacks.input.take() else { + return Some(1); + }; + let x = lparam.signed_loword(); + let y = lparam.signed_hiword(); + let physical_point = point(DevicePixels(x as i32), DevicePixels(y as i32)); + let click_count = lock.click_state.update(button, physical_point); + let scale_factor = lock.scale_factor; + drop(lock); - let mut lock = self.state.borrow_mut(); - lock.callbacks.input = Some(func); + let input = PlatformInput::MouseDown(MouseDownEvent { + button, + position: logical_point(x as f32, y as f32, scale_factor), + modifiers: current_modifiers(), + click_count, + first_mouse: false, + }); + let handled = !func(input).propagate; + state_ptr.state.borrow_mut().callbacks.input = Some(func); - if handled { - lock.system_key_handled = true; - Some(0) - } else { - // we need to call `DefWindowProcW`, or we will lose the system-wide `Alt+F4`, `Alt+{other keys}` - // shortcuts. - None - } - } + if handled { Some(0) } else { Some(1) } +} - fn handle_syskeyup_msg(&self, handle: HWND, wparam: WPARAM, lparam: LPARAM) -> Option { - let mut lock = self.state.borrow_mut(); - let input = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| { - PlatformInput::KeyUp(KeyUpEvent { keystroke }) - })?; - let mut func = lock.callbacks.input.take()?; - drop(lock); - func(input); - self.state.borrow_mut().callbacks.input = Some(func); +fn handle_mouse_up_msg( + _handle: HWND, + button: MouseButton, + lparam: LPARAM, + state_ptr: Rc, +) -> Option { + unsafe { ReleaseCapture().log_err() }; + let mut lock = state_ptr.state.borrow_mut(); + let Some(mut func) = lock.callbacks.input.take() else { + return Some(1); + }; + let x = lparam.signed_loword() as f32; + let y = lparam.signed_hiword() as f32; + let click_count = lock.click_state.current_count; + let scale_factor = lock.scale_factor; + drop(lock); - // Always return 0 to indicate that the message was handled, so we could properly handle `ModifiersChanged` event. - Some(0) - } + let input = PlatformInput::MouseUp(MouseUpEvent { + button, + position: logical_point(x, y, scale_factor), + modifiers: current_modifiers(), + click_count, + }); + let handled = !func(input).propagate; + state_ptr.state.borrow_mut().callbacks.input = Some(func); - // It's a known bug that you can't trigger `ctrl-shift-0`. See: - // https://superuser.com/questions/1455762/ctrl-shift-number-key-combination-has-stopped-working-for-a-few-numbers - fn handle_keydown_msg(&self, handle: HWND, wparam: WPARAM, lparam: LPARAM) -> Option { - let mut lock = self.state.borrow_mut(); - let Some(input) = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| { - PlatformInput::KeyDown(KeyDownEvent { - keystroke, - is_held: lparam.0 & (0x1 << 30) > 0, - }) - }) else { - return Some(1); - }; - drop(lock); + if handled { Some(0) } else { Some(1) } +} - let is_composing = self - .with_input_handler(|input_handler| input_handler.marked_text_range()) - .flatten() - .is_some(); - if is_composing { - translate_message(handle, wparam, lparam); - return Some(0); - } +fn handle_xbutton_msg( + handle: HWND, + wparam: WPARAM, + lparam: LPARAM, + handler: impl Fn(HWND, MouseButton, LPARAM, Rc) -> Option, + state_ptr: Rc, +) -> Option { + let nav_dir = match wparam.hiword() { + XBUTTON1 => NavigationDirection::Back, + XBUTTON2 => NavigationDirection::Forward, + _ => return Some(1), + }; + handler(handle, MouseButton::Navigate(nav_dir), lparam, state_ptr) +} - let Some(mut func) = self.state.borrow_mut().callbacks.input.take() else { - return Some(1); - }; +fn handle_mouse_wheel_msg( + handle: HWND, + wparam: WPARAM, + lparam: LPARAM, + state_ptr: Rc, +) -> Option { + let modifiers = current_modifiers(); + let mut lock = state_ptr.state.borrow_mut(); + let Some(mut func) = lock.callbacks.input.take() else { + return Some(1); + }; + let scale_factor = lock.scale_factor; + let wheel_scroll_amount = match modifiers.shift { + true => lock.system_settings.mouse_wheel_settings.wheel_scroll_chars, + false => lock.system_settings.mouse_wheel_settings.wheel_scroll_lines, + }; + drop(lock); - let handled = !func(input).propagate; - - self.state.borrow_mut().callbacks.input = Some(func); - - if handled { - Some(0) - } else { - translate_message(handle, wparam, lparam); - Some(1) - } - } - - fn handle_keyup_msg(&self, handle: HWND, wparam: WPARAM, lparam: LPARAM) -> Option { - let mut lock = self.state.borrow_mut(); - let Some(input) = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| { - PlatformInput::KeyUp(KeyUpEvent { keystroke }) - }) else { - return Some(1); - }; - - let Some(mut func) = lock.callbacks.input.take() else { - return Some(1); - }; - drop(lock); - - let handled = !func(input).propagate; - self.state.borrow_mut().callbacks.input = Some(func); - - if handled { Some(0) } else { Some(1) } - } - - fn handle_char_msg(&self, wparam: WPARAM) -> Option { - let input = self.parse_char_message(wparam)?; - self.with_input_handler(|input_handler| { - input_handler.replace_text_in_range(None, &input); - }); - - Some(0) - } - - fn handle_dead_char_msg(&self, wparam: WPARAM) -> Option { - let ch = char::from_u32(wparam.0 as u32)?.to_string(); - self.with_input_handler(|input_handler| { - input_handler.replace_and_mark_text_in_range(None, &ch, None); - }); - None - } - - fn handle_mouse_down_msg( - &self, - handle: HWND, - button: MouseButton, - lparam: LPARAM, - ) -> Option { - unsafe { SetCapture(handle) }; - let mut lock = self.state.borrow_mut(); - let Some(mut func) = lock.callbacks.input.take() else { - return Some(1); - }; - let x = lparam.signed_loword(); - let y = lparam.signed_hiword(); - let physical_point = point(DevicePixels(x as i32), DevicePixels(y as i32)); - let click_count = lock.click_state.update(button, physical_point); - let scale_factor = lock.scale_factor; - drop(lock); - - let input = PlatformInput::MouseDown(MouseDownEvent { - button, - position: logical_point(x as f32, y as f32, scale_factor), - modifiers: current_modifiers(), - click_count, - first_mouse: false, - }); - let handled = !func(input).propagate; - self.state.borrow_mut().callbacks.input = Some(func); - - if handled { Some(0) } else { Some(1) } - } - - fn handle_mouse_up_msg( - &self, - _handle: HWND, - button: MouseButton, - lparam: LPARAM, - ) -> Option { - unsafe { ReleaseCapture().log_err() }; - let mut lock = self.state.borrow_mut(); - let Some(mut func) = lock.callbacks.input.take() else { - return Some(1); - }; - let x = lparam.signed_loword() as f32; - let y = lparam.signed_hiword() as f32; - let click_count = lock.click_state.current_count; - let scale_factor = lock.scale_factor; - drop(lock); - - let input = PlatformInput::MouseUp(MouseUpEvent { - button, - position: logical_point(x, y, scale_factor), - modifiers: current_modifiers(), - click_count, - }); - let handled = !func(input).propagate; - self.state.borrow_mut().callbacks.input = Some(func); - - if handled { Some(0) } else { Some(1) } - } - - fn handle_xbutton_msg( - &self, - handle: HWND, - wparam: WPARAM, - lparam: LPARAM, - handler: impl Fn(&Self, HWND, MouseButton, LPARAM) -> Option, - ) -> Option { - let nav_dir = match wparam.hiword() { - XBUTTON1 => NavigationDirection::Back, - XBUTTON2 => NavigationDirection::Forward, - _ => return Some(1), - }; - handler(self, handle, MouseButton::Navigate(nav_dir), lparam) - } - - fn handle_mouse_wheel_msg( - &self, - handle: HWND, - wparam: WPARAM, - lparam: LPARAM, - ) -> Option { - let modifiers = current_modifiers(); - let mut lock = self.state.borrow_mut(); - let Some(mut func) = lock.callbacks.input.take() else { - return Some(1); - }; - let scale_factor = lock.scale_factor; - let wheel_scroll_amount = match modifiers.shift { - true => lock.system_settings.mouse_wheel_settings.wheel_scroll_chars, - false => lock.system_settings.mouse_wheel_settings.wheel_scroll_lines, - }; - drop(lock); - - let wheel_distance = - (wparam.signed_hiword() as f32 / WHEEL_DELTA as f32) * wheel_scroll_amount as f32; - let mut cursor_point = POINT { - x: lparam.signed_loword().into(), - y: lparam.signed_hiword().into(), - }; - unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; - let input = PlatformInput::ScrollWheel(ScrollWheelEvent { - position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), - delta: ScrollDelta::Lines(match modifiers.shift { - true => Point { - x: wheel_distance, - y: 0.0, - }, - false => Point { - y: wheel_distance, - x: 0.0, - }, - }), - modifiers, - touch_phase: TouchPhase::Moved, - }); - let handled = !func(input).propagate; - self.state.borrow_mut().callbacks.input = Some(func); - - if handled { Some(0) } else { Some(1) } - } - - fn handle_mouse_horizontal_wheel_msg( - &self, - handle: HWND, - wparam: WPARAM, - lparam: LPARAM, - ) -> Option { - let mut lock = self.state.borrow_mut(); - let Some(mut func) = lock.callbacks.input.take() else { - return Some(1); - }; - let scale_factor = lock.scale_factor; - let wheel_scroll_chars = lock.system_settings.mouse_wheel_settings.wheel_scroll_chars; - drop(lock); - - let wheel_distance = - (-wparam.signed_hiword() as f32 / WHEEL_DELTA as f32) * wheel_scroll_chars as f32; - let mut cursor_point = POINT { - x: lparam.signed_loword().into(), - y: lparam.signed_hiword().into(), - }; - unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; - let event = PlatformInput::ScrollWheel(ScrollWheelEvent { - position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), - delta: ScrollDelta::Lines(Point { + let wheel_distance = + (wparam.signed_hiword() as f32 / WHEEL_DELTA as f32) * wheel_scroll_amount as f32; + let mut cursor_point = POINT { + x: lparam.signed_loword().into(), + y: lparam.signed_hiword().into(), + }; + unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; + let input = PlatformInput::ScrollWheel(ScrollWheelEvent { + position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), + delta: ScrollDelta::Lines(match modifiers.shift { + true => Point { x: wheel_distance, y: 0.0, - }), - modifiers: current_modifiers(), - touch_phase: TouchPhase::Moved, - }); - let handled = !func(event).propagate; - self.state.borrow_mut().callbacks.input = Some(func); + }, + false => Point { + y: wheel_distance, + x: 0.0, + }, + }), + modifiers, + touch_phase: TouchPhase::Moved, + }); + let handled = !func(input).propagate; + state_ptr.state.borrow_mut().callbacks.input = Some(func); - if handled { Some(0) } else { Some(1) } - } + if handled { Some(0) } else { Some(1) } +} - fn retrieve_caret_position(&self) -> Option { - self.with_input_handler_and_scale_factor(|input_handler, scale_factor| { - let caret_range = input_handler.selected_text_range(false)?; - let caret_position = input_handler.bounds_for_range(caret_range.range)?; - Some(POINT { - // logical to physical - x: (caret_position.origin.x.0 * scale_factor) as i32, - y: (caret_position.origin.y.0 * scale_factor) as i32 - + ((caret_position.size.height.0 * scale_factor) as i32 / 2), - }) +fn handle_mouse_horizontal_wheel_msg( + handle: HWND, + wparam: WPARAM, + lparam: LPARAM, + state_ptr: Rc, +) -> Option { + let mut lock = state_ptr.state.borrow_mut(); + let Some(mut func) = lock.callbacks.input.take() else { + return Some(1); + }; + let scale_factor = lock.scale_factor; + let wheel_scroll_chars = lock.system_settings.mouse_wheel_settings.wheel_scroll_chars; + drop(lock); + + let wheel_distance = + (-wparam.signed_hiword() as f32 / WHEEL_DELTA as f32) * wheel_scroll_chars as f32; + let mut cursor_point = POINT { + x: lparam.signed_loword().into(), + y: lparam.signed_hiword().into(), + }; + unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; + let event = PlatformInput::ScrollWheel(ScrollWheelEvent { + position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), + delta: ScrollDelta::Lines(Point { + x: wheel_distance, + y: 0.0, + }), + modifiers: current_modifiers(), + touch_phase: TouchPhase::Moved, + }); + let handled = !func(event).propagate; + state_ptr.state.borrow_mut().callbacks.input = Some(func); + + if handled { Some(0) } else { Some(1) } +} + +fn retrieve_caret_position(state_ptr: &Rc) -> Option { + with_input_handler_and_scale_factor(state_ptr, |input_handler, scale_factor| { + let caret_range = input_handler.selected_text_range(false)?; + let caret_position = input_handler.bounds_for_range(caret_range.range)?; + Some(POINT { + // logical to physical + x: (caret_position.origin.x.0 * scale_factor) as i32, + y: (caret_position.origin.y.0 * scale_factor) as i32 + + ((caret_position.size.height.0 * scale_factor) as i32 / 2), }) - } + }) +} - fn handle_ime_position(&self, handle: HWND) -> Option { - unsafe { - let ctx = ImmGetContext(handle); +fn handle_ime_position(handle: HWND, state_ptr: Rc) -> Option { + unsafe { + let ctx = ImmGetContext(handle); - let Some(caret_position) = self.retrieve_caret_position() else { - return Some(0); + let Some(caret_position) = retrieve_caret_position(&state_ptr) else { + return Some(0); + }; + { + let config = COMPOSITIONFORM { + dwStyle: CFS_POINT, + ptCurrentPos: caret_position, + ..Default::default() }; - { - let config = COMPOSITIONFORM { - dwStyle: CFS_POINT, - ptCurrentPos: caret_position, - ..Default::default() - }; - ImmSetCompositionWindow(ctx, &config as _).ok().log_err(); - } - { - let config = CANDIDATEFORM { - dwStyle: CFS_CANDIDATEPOS, - ptCurrentPos: caret_position, - ..Default::default() - }; - ImmSetCandidateWindow(ctx, &config as _).ok().log_err(); - } - ImmReleaseContext(handle, ctx).ok().log_err(); - Some(0) + ImmSetCompositionWindow(ctx, &config as _).ok().log_err(); } + { + let config = CANDIDATEFORM { + dwStyle: CFS_CANDIDATEPOS, + ptCurrentPos: caret_position, + ..Default::default() + }; + ImmSetCandidateWindow(ctx, &config as _).ok().log_err(); + } + ImmReleaseContext(handle, ctx).ok().log_err(); + Some(0) } +} - fn handle_ime_composition(&self, handle: HWND, lparam: LPARAM) -> Option { - let ctx = unsafe { ImmGetContext(handle) }; - let result = self.handle_ime_composition_inner(ctx, lparam); - unsafe { ImmReleaseContext(handle, ctx).ok().log_err() }; - result - } +fn handle_ime_composition( + handle: HWND, + lparam: LPARAM, + state_ptr: Rc, +) -> Option { + let ctx = unsafe { ImmGetContext(handle) }; + let result = handle_ime_composition_inner(ctx, lparam, state_ptr); + unsafe { ImmReleaseContext(handle, ctx).ok().log_err() }; + result +} - fn handle_ime_composition_inner(&self, ctx: HIMC, lparam: LPARAM) -> Option { - let lparam = lparam.0 as u32; - if lparam == 0 { - // Japanese IME may send this message with lparam = 0, which indicates that - // there is no composition string. - self.with_input_handler(|input_handler| { - input_handler.replace_text_in_range(None, ""); +fn handle_ime_composition_inner( + ctx: HIMC, + lparam: LPARAM, + state_ptr: Rc, +) -> Option { + let lparam = lparam.0 as u32; + if lparam == 0 { + // Japanese IME may send this message with lparam = 0, which indicates that + // there is no composition string. + with_input_handler(&state_ptr, |input_handler| { + input_handler.replace_text_in_range(None, ""); + })?; + Some(0) + } else { + if lparam & GCS_COMPSTR.0 > 0 { + let comp_string = parse_ime_composition_string(ctx, GCS_COMPSTR)?; + let caret_pos = (!comp_string.is_empty() && lparam & GCS_CURSORPOS.0 > 0).then(|| { + let pos = retrieve_composition_cursor_position(ctx); + pos..pos + }); + with_input_handler(&state_ptr, |input_handler| { + input_handler.replace_and_mark_text_in_range(None, &comp_string, caret_pos); })?; - Some(0) - } else { - if lparam & GCS_COMPSTR.0 > 0 { - let comp_string = parse_ime_composition_string(ctx, GCS_COMPSTR)?; - let caret_pos = - (!comp_string.is_empty() && lparam & GCS_CURSORPOS.0 > 0).then(|| { - let pos = retrieve_composition_cursor_position(ctx); - pos..pos - }); - self.with_input_handler(|input_handler| { - input_handler.replace_and_mark_text_in_range(None, &comp_string, caret_pos); - })?; - } - if lparam & GCS_RESULTSTR.0 > 0 { - let comp_result = parse_ime_composition_string(ctx, GCS_RESULTSTR)?; - self.with_input_handler(|input_handler| { - input_handler.replace_text_in_range(None, &comp_result); - })?; - return Some(0); - } - - // currently, we don't care other stuff - None } + if lparam & GCS_RESULTSTR.0 > 0 { + let comp_result = parse_ime_composition_string(ctx, GCS_RESULTSTR)?; + with_input_handler(&state_ptr, |input_handler| { + input_handler.replace_text_in_range(None, &comp_result); + })?; + return Some(0); + } + + // currently, we don't care other stuff + None + } +} + +/// SEE: https://learn.microsoft.com/en-us/windows/win32/winmsg/wm-nccalcsize +fn handle_calc_client_size( + handle: HWND, + wparam: WPARAM, + lparam: LPARAM, + state_ptr: Rc, +) -> Option { + if !state_ptr.hide_title_bar || state_ptr.state.borrow().is_fullscreen() || wparam.0 == 0 { + return None; } - /// SEE: https://learn.microsoft.com/en-us/windows/win32/winmsg/wm-nccalcsize - fn handle_calc_client_size( - &self, - handle: HWND, - wparam: WPARAM, - lparam: LPARAM, - ) -> Option { - if !self.hide_title_bar || self.state.borrow().is_fullscreen() || wparam.0 == 0 { - return None; - } + let is_maximized = state_ptr.state.borrow().is_maximized(); + let insets = get_client_area_insets(handle, is_maximized, state_ptr.windows_version); + // wparam is TRUE so lparam points to an NCCALCSIZE_PARAMS structure + let mut params = lparam.0 as *mut NCCALCSIZE_PARAMS; + let mut requested_client_rect = unsafe { &mut ((*params).rgrc) }; - let is_maximized = self.state.borrow().is_maximized(); - let insets = get_client_area_insets(handle, is_maximized, self.windows_version); - // wparam is TRUE so lparam points to an NCCALCSIZE_PARAMS structure - let mut params = lparam.0 as *mut NCCALCSIZE_PARAMS; - let mut requested_client_rect = unsafe { &mut ((*params).rgrc) }; + requested_client_rect[0].left += insets.left; + requested_client_rect[0].top += insets.top; + requested_client_rect[0].right -= insets.right; + requested_client_rect[0].bottom -= insets.bottom; - requested_client_rect[0].left += insets.left; - requested_client_rect[0].top += insets.top; - requested_client_rect[0].right -= insets.right; - requested_client_rect[0].bottom -= insets.bottom; - - // 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 - && let Some(ref taskbar_position) = self - .state - .borrow() - .system_settings - .auto_hide_taskbar_position + // 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) = state_ptr + .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 @@ -726,191 +757,272 @@ impl WindowsWindowInner { } } } - - Some(0) } - fn handle_activate_msg(self: &Rc, wparam: WPARAM) -> Option { - let activated = wparam.loword() > 0; - let this = self.clone(); - self.executor - .spawn(async move { - let mut lock = this.state.borrow_mut(); - if let Some(mut func) = lock.callbacks.active_status_change.take() { - drop(lock); - func(activated); - this.state.borrow_mut().callbacks.active_status_change = Some(func); - } - }) - .detach(); + Some(0) +} +fn handle_activate_msg(wparam: WPARAM, state_ptr: Rc) -> Option { + let activated = wparam.loword() > 0; + let this = state_ptr.clone(); + state_ptr + .executor + .spawn(async move { + let mut lock = this.state.borrow_mut(); + if let Some(mut func) = lock.callbacks.active_status_change.take() { + drop(lock); + func(activated); + this.state.borrow_mut().callbacks.active_status_change = Some(func); + } + }) + .detach(); + + None +} + +fn handle_create_msg(handle: HWND, state_ptr: Rc) -> Option { + if state_ptr.hide_title_bar { + notify_frame_changed(handle); + Some(0) + } else { None } +} - fn handle_create_msg(&self, handle: HWND) -> Option { - if self.hide_title_bar { - notify_frame_changed(handle); - Some(0) - } else { - None - } +fn handle_dpi_changed_msg( + handle: HWND, + wparam: WPARAM, + lparam: LPARAM, + state_ptr: Rc, +) -> Option { + let new_dpi = wparam.loword() as f32; + let mut lock = state_ptr.state.borrow_mut(); + lock.scale_factor = new_dpi / USER_DEFAULT_SCREEN_DPI as f32; + lock.border_offset.update(handle).log_err(); + drop(lock); + + let rect = unsafe { &*(lparam.0 as *const RECT) }; + let width = rect.right - rect.left; + let height = rect.bottom - rect.top; + // this will emit `WM_SIZE` and `WM_MOVE` right here + // even before this function returns + // the new size is handled in `WM_SIZE` + unsafe { + SetWindowPos( + handle, + None, + rect.left, + rect.top, + width, + height, + SWP_NOZORDER | SWP_NOACTIVATE, + ) + .context("unable to set window position after dpi has changed") + .log_err(); } - fn handle_dpi_changed_msg( - &self, - handle: HWND, - wparam: WPARAM, - lparam: LPARAM, - ) -> Option { - let new_dpi = wparam.loword() as f32; - let mut lock = self.state.borrow_mut(); - let is_maximized = lock.is_maximized(); - let new_scale_factor = new_dpi / USER_DEFAULT_SCREEN_DPI as f32; - lock.scale_factor = new_scale_factor; - lock.border_offset.update(handle).log_err(); + Some(0) +} + +/// The following conditions will trigger this event: +/// 1. The monitor on which the window is located goes offline or changes resolution. +/// 2. Another monitor goes offline, is plugged in, or changes resolution. +/// +/// In either case, the window will only receive information from the monitor on which +/// it is located. +/// +/// For example, in the case of condition 2, where the monitor on which the window is +/// located has actually changed nothing, it will still receive this event. +fn handle_display_change_msg(handle: HWND, state_ptr: Rc) -> Option { + // NOTE: + // Even the `lParam` holds the resolution of the screen, we just ignore it. + // Because WM_DPICHANGED, WM_MOVE, WM_SIZE will come first, window reposition and resize + // are handled there. + // So we only care about if monitor is disconnected. + let previous_monitor = state_ptr.state.borrow().display; + if WindowsDisplay::is_connected(previous_monitor.handle) { + // we are fine, other display changed + return None; + } + // display disconnected + // in this case, the OS will move our window to another monitor, and minimize it. + // we deminimize the window and query the monitor after moving + unsafe { + let _ = ShowWindow(handle, SW_SHOWNORMAL); + }; + let new_monitor = unsafe { MonitorFromWindow(handle, MONITOR_DEFAULTTONULL) }; + // all monitors disconnected + if new_monitor.is_invalid() { + log::error!("No monitor detected!"); + return None; + } + let new_display = WindowsDisplay::new_with_handle(new_monitor); + state_ptr.state.borrow_mut().display = new_display; + Some(0) +} + +fn handle_hit_test_msg( + handle: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, + state_ptr: Rc, +) -> Option { + if !state_ptr.is_movable || state_ptr.state.borrow().is_fullscreen() { + return None; + } + + let mut lock = state_ptr.state.borrow_mut(); + if let Some(mut callback) = lock.callbacks.hit_test_window_control.take() { drop(lock); - - let rect = unsafe { &*(lparam.0 as *const RECT) }; - let width = rect.right - rect.left; - let height = rect.bottom - rect.top; - // this will emit `WM_SIZE` and `WM_MOVE` right here - // even before this function returns - // the new size is handled in `WM_SIZE` - unsafe { - SetWindowPos( - handle, - None, - rect.left, - rect.top, - width, - height, - SWP_NOZORDER | SWP_NOACTIVATE, - ) - .context("unable to set window position after dpi has changed") - .log_err(); + let area = callback(); + state_ptr + .state + .borrow_mut() + .callbacks + .hit_test_window_control = Some(callback); + if let Some(area) = area { + return match area { + WindowControlArea::Drag => Some(HTCAPTION as _), + WindowControlArea::Close => Some(HTCLOSE as _), + WindowControlArea::Max => Some(HTMAXBUTTON as _), + WindowControlArea::Min => Some(HTMINBUTTON as _), + }; } - - // When maximized, SetWindowPos doesn't send WM_SIZE, so we need to manually - // update the size and call the resize callback - if is_maximized { - let device_size = size(DevicePixels(width), DevicePixels(height)); - self.handle_size_change(device_size, new_scale_factor, true); - } - - Some(0) + } else { + drop(lock); } - /// The following conditions will trigger this event: - /// 1. The monitor on which the window is located goes offline or changes resolution. - /// 2. Another monitor goes offline, is plugged in, or changes resolution. - /// - /// In either case, the window will only receive information from the monitor on which - /// it is located. - /// - /// For example, in the case of condition 2, where the monitor on which the window is - /// located has actually changed nothing, it will still receive this event. - fn handle_display_change_msg(&self, handle: HWND) -> Option { - // NOTE: - // Even the `lParam` holds the resolution of the screen, we just ignore it. - // Because WM_DPICHANGED, WM_MOVE, WM_SIZE will come first, window reposition and resize - // are handled there. - // So we only care about if monitor is disconnected. - let previous_monitor = self.state.borrow().display; - if WindowsDisplay::is_connected(previous_monitor.handle) { - // we are fine, other display changed - return None; - } - // display disconnected - // in this case, the OS will move our window to another monitor, and minimize it. - // we deminimize the window and query the monitor after moving - unsafe { - let _ = ShowWindow(handle, SW_SHOWNORMAL); - }; - let new_monitor = unsafe { MonitorFromWindow(handle, MONITOR_DEFAULTTONULL) }; - // all monitors disconnected - if new_monitor.is_invalid() { - log::error!("No monitor detected!"); - return None; - } - let new_display = WindowsDisplay::new_with_handle(new_monitor); - self.state.borrow_mut().display = new_display; - Some(0) + if !state_ptr.hide_title_bar { + // If the OS draws the title bar, we don't need to handle hit test messages. + return None; } - fn handle_hit_test_msg( - &self, - handle: HWND, - msg: u32, - wparam: WPARAM, - lparam: LPARAM, - ) -> Option { - if !self.is_movable || self.state.borrow().is_fullscreen() { - return None; - } + // default handler for resize areas + let hit = unsafe { DefWindowProcW(handle, msg, wparam, lparam) }; + if matches!( + hit.0 as u32, + HTNOWHERE + | HTRIGHT + | HTLEFT + | HTTOPLEFT + | HTTOP + | HTTOPRIGHT + | HTBOTTOMRIGHT + | HTBOTTOM + | HTBOTTOMLEFT + ) { + return Some(hit.0); + } - let mut lock = self.state.borrow_mut(); - if let Some(mut callback) = lock.callbacks.hit_test_window_control.take() { - drop(lock); - let area = callback(); - self.state.borrow_mut().callbacks.hit_test_window_control = Some(callback); - if let Some(area) = area { - return match area { - WindowControlArea::Drag => Some(HTCAPTION as _), - WindowControlArea::Close => Some(HTCLOSE as _), - WindowControlArea::Max => Some(HTMAXBUTTON as _), - WindowControlArea::Min => Some(HTMINBUTTON as _), - }; - } - } else { - drop(lock); - } + if state_ptr.state.borrow().is_fullscreen() { + return Some(HTCLIENT as _); + } - if !self.hide_title_bar { - // If the OS draws the title bar, we don't need to handle hit test messages. - return None; - } + let dpi = unsafe { GetDpiForWindow(handle) }; + let frame_y = unsafe { GetSystemMetricsForDpi(SM_CYFRAME, dpi) }; - // default handler for resize areas - let hit = unsafe { DefWindowProcW(handle, msg, wparam, lparam) }; - if matches!( - hit.0 as u32, - HTNOWHERE - | HTRIGHT - | HTLEFT - | HTTOPLEFT - | HTTOP - | HTTOPRIGHT - | HTBOTTOMRIGHT - | HTBOTTOM - | HTBOTTOMLEFT - ) { - return Some(hit.0); - } + let mut cursor_point = POINT { + x: lparam.signed_loword().into(), + y: lparam.signed_hiword().into(), + }; + unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; + if !state_ptr.state.borrow().is_maximized() && cursor_point.y >= 0 && cursor_point.y <= frame_y + { + return Some(HTTOP as _); + } - if self.state.borrow().is_fullscreen() { - return Some(HTCLIENT as _); - } + Some(HTCLIENT as _) +} - let dpi = unsafe { GetDpiForWindow(handle) }; - let frame_y = unsafe { GetSystemMetricsForDpi(SM_CYFRAME, dpi) }; +fn handle_nc_mouse_move_msg( + handle: HWND, + lparam: LPARAM, + state_ptr: Rc, +) -> Option { + start_tracking_mouse(handle, &state_ptr, TME_LEAVE | TME_NONCLIENT); + let mut lock = state_ptr.state.borrow_mut(); + let mut func = lock.callbacks.input.take()?; + let scale_factor = lock.scale_factor; + drop(lock); + + let mut cursor_point = POINT { + x: lparam.signed_loword().into(), + y: lparam.signed_hiword().into(), + }; + unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; + let input = PlatformInput::MouseMove(MouseMoveEvent { + position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), + pressed_button: None, + modifiers: current_modifiers(), + }); + let handled = !func(input).propagate; + state_ptr.state.borrow_mut().callbacks.input = Some(func); + + if handled { Some(0) } else { None } +} + +fn handle_nc_mouse_down_msg( + handle: HWND, + button: MouseButton, + wparam: WPARAM, + lparam: LPARAM, + state_ptr: Rc, +) -> Option { + let mut lock = state_ptr.state.borrow_mut(); + if let Some(mut func) = lock.callbacks.input.take() { + let scale_factor = lock.scale_factor; let mut cursor_point = POINT { x: lparam.signed_loword().into(), y: lparam.signed_hiword().into(), }; unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; - if !self.state.borrow().is_maximized() && cursor_point.y >= 0 && cursor_point.y <= frame_y { - return Some(HTTOP as _); + let physical_point = point(DevicePixels(cursor_point.x), DevicePixels(cursor_point.y)); + let click_count = lock.click_state.update(button, physical_point); + drop(lock); + + let input = PlatformInput::MouseDown(MouseDownEvent { + button, + position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), + modifiers: current_modifiers(), + click_count, + first_mouse: false, + }); + let result = func(input.clone()); + let handled = !result.propagate || result.default_prevented; + state_ptr.state.borrow_mut().callbacks.input = Some(func); + + if handled { + return Some(0); } + } else { + drop(lock); + }; - Some(HTCLIENT as _) + // Since these are handled in handle_nc_mouse_up_msg we must prevent the default window proc + if button == MouseButton::Left { + match wparam.0 as u32 { + HTMINBUTTON => state_ptr.state.borrow_mut().nc_button_pressed = Some(HTMINBUTTON), + HTMAXBUTTON => state_ptr.state.borrow_mut().nc_button_pressed = Some(HTMAXBUTTON), + HTCLOSE => state_ptr.state.borrow_mut().nc_button_pressed = Some(HTCLOSE), + _ => return None, + }; + Some(0) + } else { + None } +} - fn handle_nc_mouse_move_msg(&self, handle: HWND, lparam: LPARAM) -> Option { - self.start_tracking_mouse(handle, TME_LEAVE | TME_NONCLIENT); - - let mut lock = self.state.borrow_mut(); - let mut func = lock.callbacks.input.take()?; +fn handle_nc_mouse_up_msg( + handle: HWND, + button: MouseButton, + wparam: WPARAM, + lparam: LPARAM, + state_ptr: Rc, +) -> Option { + let mut lock = state_ptr.state.borrow_mut(); + if let Some(mut func) = lock.callbacks.input.take() { let scale_factor = lock.scale_factor; drop(lock); @@ -919,358 +1031,262 @@ impl WindowsWindowInner { y: lparam.signed_hiword().into(), }; unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; - let input = PlatformInput::MouseMove(MouseMoveEvent { + let input = PlatformInput::MouseUp(MouseUpEvent { + button, position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), - pressed_button: None, modifiers: current_modifiers(), + click_count: 1, }); let handled = !func(input).propagate; - self.state.borrow_mut().callbacks.input = Some(func); + state_ptr.state.borrow_mut().callbacks.input = Some(func); - if handled { Some(0) } else { None } + if handled { + return Some(0); + } + } else { + drop(lock); } - fn handle_nc_mouse_down_msg( - &self, - handle: HWND, - button: MouseButton, - wparam: WPARAM, - lparam: LPARAM, - ) -> Option { - let mut lock = self.state.borrow_mut(); - if let Some(mut func) = lock.callbacks.input.take() { - let scale_factor = lock.scale_factor; - let mut cursor_point = POINT { - x: lparam.signed_loword().into(), - y: lparam.signed_hiword().into(), - }; - unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; - let physical_point = point(DevicePixels(cursor_point.x), DevicePixels(cursor_point.y)); - let click_count = lock.click_state.update(button, physical_point); - drop(lock); - - let input = PlatformInput::MouseDown(MouseDownEvent { - button, - position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), - modifiers: current_modifiers(), - click_count, - first_mouse: false, - }); - let result = func(input); - let handled = !result.propagate || result.default_prevented; - self.state.borrow_mut().callbacks.input = Some(func); - - if handled { - return Some(0); + let last_pressed = state_ptr.state.borrow_mut().nc_button_pressed.take(); + if button == MouseButton::Left + && let Some(last_pressed) = last_pressed + { + let handled = match (wparam.0 as u32, last_pressed) { + (HTMINBUTTON, HTMINBUTTON) => { + unsafe { ShowWindowAsync(handle, SW_MINIMIZE).ok().log_err() }; + true } - } else { - drop(lock); + (HTMAXBUTTON, HTMAXBUTTON) => { + if state_ptr.state.borrow().is_maximized() { + unsafe { ShowWindowAsync(handle, SW_NORMAL).ok().log_err() }; + } else { + unsafe { ShowWindowAsync(handle, SW_MAXIMIZE).ok().log_err() }; + } + true + } + (HTCLOSE, HTCLOSE) => { + unsafe { + PostMessageW(Some(handle), WM_CLOSE, WPARAM::default(), LPARAM::default()) + .log_err() + }; + true + } + _ => false, }; - - // Since these are handled in handle_nc_mouse_up_msg we must prevent the default window proc - if button == MouseButton::Left { - match wparam.0 as u32 { - HTMINBUTTON => self.state.borrow_mut().nc_button_pressed = Some(HTMINBUTTON), - HTMAXBUTTON => self.state.borrow_mut().nc_button_pressed = Some(HTMAXBUTTON), - HTCLOSE => self.state.borrow_mut().nc_button_pressed = Some(HTCLOSE), - _ => return None, - }; - Some(0) - } else { - None + if handled { + return Some(0); } } - fn handle_nc_mouse_up_msg( - &self, - handle: HWND, - button: MouseButton, - wparam: WPARAM, - lparam: LPARAM, - ) -> Option { - let mut lock = self.state.borrow_mut(); - if let Some(mut func) = lock.callbacks.input.take() { - let scale_factor = lock.scale_factor; - drop(lock); + None +} - let mut cursor_point = POINT { - x: lparam.signed_loword().into(), - y: lparam.signed_hiword().into(), - }; - unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() }; - let input = PlatformInput::MouseUp(MouseUpEvent { - button, - position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), - modifiers: current_modifiers(), - click_count: 1, - }); - let handled = !func(input).propagate; - self.state.borrow_mut().callbacks.input = Some(func); - - if handled { - return Some(0); - } - } else { - drop(lock); - } - - let last_pressed = self.state.borrow_mut().nc_button_pressed.take(); - if button == MouseButton::Left - && let Some(last_pressed) = last_pressed - { - let handled = match (wparam.0 as u32, last_pressed) { - (HTMINBUTTON, HTMINBUTTON) => { - unsafe { ShowWindowAsync(handle, SW_MINIMIZE).ok().log_err() }; - true - } - (HTMAXBUTTON, HTMAXBUTTON) => { - if self.state.borrow().is_maximized() { - unsafe { ShowWindowAsync(handle, SW_NORMAL).ok().log_err() }; - } else { - unsafe { ShowWindowAsync(handle, SW_MAXIMIZE).ok().log_err() }; - } - true - } - (HTCLOSE, HTCLOSE) => { - unsafe { - PostMessageW(Some(handle), WM_CLOSE, WPARAM::default(), LPARAM::default()) - .log_err() - }; - true - } - _ => false, - }; - if handled { - return Some(0); - } - } +fn handle_cursor_changed(lparam: LPARAM, state_ptr: Rc) -> Option { + let mut state = state_ptr.state.borrow_mut(); + let had_cursor = state.current_cursor.is_some(); + state.current_cursor = if lparam.0 == 0 { None + } else { + Some(HCURSOR(lparam.0 as _)) + }; + + if had_cursor != state.current_cursor.is_some() { + unsafe { SetCursor(state.current_cursor) }; } - fn handle_cursor_changed(&self, lparam: LPARAM) -> Option { - let mut state = self.state.borrow_mut(); - let had_cursor = state.current_cursor.is_some(); + Some(0) +} - state.current_cursor = if lparam.0 == 0 { - None - } else { - Some(HCURSOR(lparam.0 as _)) - }; +fn handle_set_cursor( + handle: HWND, + lparam: LPARAM, + state_ptr: Rc, +) -> Option { + if unsafe { !IsWindowEnabled(handle).as_bool() } + || matches!( + lparam.loword() as u32, + HTLEFT + | HTRIGHT + | HTTOP + | HTTOPLEFT + | HTTOPRIGHT + | HTBOTTOM + | HTBOTTOMLEFT + | HTBOTTOMRIGHT + ) + { + return None; + } + unsafe { + SetCursor(state_ptr.state.borrow().current_cursor); + }; + Some(1) +} - if had_cursor != state.current_cursor.is_some() { - unsafe { SetCursor(state.current_cursor) }; +fn handle_system_settings_changed( + handle: HWND, + wparam: WPARAM, + lparam: LPARAM, + state_ptr: Rc, +) -> Option { + if wparam.0 != 0 { + let mut lock = state_ptr.state.borrow_mut(); + let display = lock.display; + lock.system_settings.update(display, wparam.0); + lock.click_state.system_update(wparam.0); + lock.border_offset.update(handle).log_err(); + } else { + handle_system_theme_changed(handle, lparam, state_ptr)?; + }; + // Force to trigger WM_NCCALCSIZE event to ensure that we handle auto hide + // taskbar correctly. + notify_frame_changed(handle); + + Some(0) +} + +fn handle_system_command(wparam: WPARAM, state_ptr: Rc) -> Option { + if wparam.0 == SC_KEYMENU as usize { + let mut lock = state_ptr.state.borrow_mut(); + if lock.system_key_handled { + lock.system_key_handled = false; + return Some(0); } - - Some(0) } + None +} - fn handle_set_cursor(&self, handle: HWND, lparam: LPARAM) -> Option { - if unsafe { !IsWindowEnabled(handle).as_bool() } - || matches!( - lparam.loword() as u32, - HTLEFT - | HTRIGHT - | HTTOP - | HTTOPLEFT - | HTTOPRIGHT - | HTBOTTOM - | HTBOTTOMLEFT - | HTBOTTOMRIGHT - ) - { - return None; - } - unsafe { - SetCursor(self.state.borrow().current_cursor); - }; - Some(1) - } - - fn handle_system_settings_changed( - &self, - handle: HWND, - wparam: WPARAM, - lparam: LPARAM, - ) -> Option { - if wparam.0 != 0 { - let mut lock = self.state.borrow_mut(); - let display = lock.display; - lock.system_settings.update(display, wparam.0); - lock.click_state.system_update(wparam.0); - lock.border_offset.update(handle).log_err(); - } else { - self.handle_system_theme_changed(handle, lparam)?; - }; - // Force to trigger WM_NCCALCSIZE event to ensure that we handle auto hide - // taskbar correctly. - notify_frame_changed(handle); - - Some(0) - } - - fn handle_system_command(&self, wparam: WPARAM) -> Option { - if wparam.0 == SC_KEYMENU as usize { - let mut lock = self.state.borrow_mut(); - if lock.system_key_handled { - lock.system_key_handled = false; - return Some(0); - } - } - None - } - - fn handle_system_theme_changed(&self, handle: HWND, lparam: LPARAM) -> Option { - // 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() } - && let Some(parameter_string) = unsafe { parameter.to_string() }.log_err() - { +fn handle_system_theme_changed( + handle: HWND, + lparam: LPARAM, + state_ptr: Rc, +) -> Option { + // 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); - 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); + 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 = state_ptr.state.borrow_mut(); + if new_appearance != lock.appearance { + lock.appearance = new_appearance; + let mut callback = lock.callbacks.appearance_changed.take()?; + drop(lock); + callback(); + state_ptr.state.borrow_mut().callbacks.appearance_changed = Some(callback); + configure_dwm_dark_mode(handle, new_appearance); + } } + _ => {} } } - Some(0) } + Some(0) +} - fn handle_input_language_changed(&self, lparam: LPARAM) -> Option { - let thread = self.main_thread_id_win32; - let validation = self.validation_number; +fn handle_input_language_changed( + lparam: LPARAM, + state_ptr: Rc, +) -> Option { + let thread = state_ptr.main_thread_id_win32; + let validation = state_ptr.validation_number; + unsafe { + PostThreadMessageW(thread, WM_INPUTLANGCHANGE, WPARAM(validation), lparam).log_err(); + } + Some(0) +} + +fn handle_device_change_msg( + handle: HWND, + wparam: WPARAM, + state_ptr: Rc, +) -> Option { + if wparam.0 == DBT_DEVNODES_CHANGED as usize { + // The reason for sending this message is to actually trigger a redraw of the window. unsafe { - PostThreadMessageW(thread, WM_INPUTLANGCHANGE, WPARAM(validation), lparam).log_err(); - } - Some(0) - } - - fn handle_window_visibility_changed(&self, handle: HWND, wparam: WPARAM) -> Option { - if wparam.0 == 1 { - self.draw_window(handle, false); + 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. + draw_window(handle, true, state_ptr) + } else { + // Other device change messages are not handled. None } +} - fn handle_device_change_msg(&self, handle: HWND, wparam: WPARAM) -> Option { - 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. +#[inline] +fn draw_window( + handle: HWND, + force_render: bool, + state_ptr: Rc, +) -> Option { + let mut request_frame = state_ptr + .state + .borrow_mut() + .callbacks + .request_frame + .take()?; + request_frame(RequestFrameOptions { + require_presentation: true, + force_render, + }); + let mut lock = state_ptr.state.borrow_mut(); + if let Some(keydown_time) = lock.keydown_time.take() { + let elapsed = keydown_time.elapsed(); + println!( + "Elapsed keydown time: {:.02} ms", + elapsed.as_secs_f64() * 1000.0 + ); + } + lock.callbacks.request_frame = Some(request_frame); + unsafe { ValidateRect(Some(handle), None).ok().log_err() }; + Some(0) +} + +#[inline] +fn parse_char_message(wparam: WPARAM, state_ptr: &Rc) -> Option { + let code_point = wparam.loword(); + let mut lock = state_ptr.state.borrow_mut(); + // https://www.unicode.org/versions/Unicode16.0.0/core-spec/chapter-3/#G2630 + match code_point { + 0xD800..=0xDBFF => { + // High surrogate, wait for low surrogate + lock.pending_surrogate = Some(code_point); None } - } - - #[inline] - fn draw_window(&self, handle: HWND, force_render: bool) -> Option { - let mut request_frame = self.state.borrow_mut().callbacks.request_frame.take()?; - request_frame(RequestFrameOptions { - require_presentation: false, - force_render, - }); - self.state.borrow_mut().callbacks.request_frame = Some(request_frame); - unsafe { ValidateRect(Some(handle), None).ok().log_err() }; - Some(0) - } - - #[inline] - fn parse_char_message(&self, wparam: WPARAM) -> Option { - let code_point = wparam.loword(); - let mut lock = self.state.borrow_mut(); - // https://www.unicode.org/versions/Unicode16.0.0/core-spec/chapter-3/#G2630 - match code_point { - 0xD800..=0xDBFF => { - // High surrogate, wait for low surrogate - lock.pending_surrogate = Some(code_point); + 0xDC00..=0xDFFF => { + if let Some(high_surrogate) = lock.pending_surrogate.take() { + // Low surrogate, combine with pending high surrogate + String::from_utf16(&[high_surrogate, code_point]).ok() + } else { + // Invalid low surrogate without a preceding high surrogate + log::warn!( + "Received low surrogate without a preceding high surrogate: {code_point:x}" + ); None } - 0xDC00..=0xDFFF => { - if let Some(high_surrogate) = lock.pending_surrogate.take() { - // Low surrogate, combine with pending high surrogate - String::from_utf16(&[high_surrogate, code_point]).ok() - } else { - // Invalid low surrogate without a preceding high surrogate - log::warn!( - "Received low surrogate without a preceding high surrogate: {code_point:x}" - ); - None - } - } - _ => { - lock.pending_surrogate = None; - char::from_u32(code_point as u32) - .filter(|c| !c.is_control()) - .map(|c| c.to_string()) - } } - } - - fn start_tracking_mouse(&self, handle: HWND, flags: TRACKMOUSEEVENT_FLAGS) { - let mut lock = self.state.borrow_mut(); - if !lock.hovered { - lock.hovered = true; - unsafe { - TrackMouseEvent(&mut TRACKMOUSEEVENT { - cbSize: std::mem::size_of::() as u32, - dwFlags: flags, - hwndTrack: handle, - dwHoverTime: HOVER_DEFAULT, - }) - .log_err() - }; - if let Some(mut callback) = lock.callbacks.hovered_status_change.take() { - drop(lock); - callback(true); - self.state.borrow_mut().callbacks.hovered_status_change = Some(callback); - } + _ => { + lock.pending_surrogate = None; + char::from_u32(code_point as u32) + .filter(|c| !c.is_control()) + .map(|c| c.to_string()) } } - - fn with_input_handler(&self, f: F) -> Option - where - F: FnOnce(&mut PlatformInputHandler) -> R, - { - let mut input_handler = self.state.borrow_mut().input_handler.take()?; - let result = f(&mut input_handler); - self.state.borrow_mut().input_handler = Some(input_handler); - Some(result) - } - - fn with_input_handler_and_scale_factor(&self, f: F) -> Option - where - F: FnOnce(&mut PlatformInputHandler, f32) -> Option, - { - let mut lock = self.state.borrow_mut(); - let mut input_handler = lock.input_handler.take()?; - let scale_factor = lock.scale_factor; - drop(lock); - let result = f(&mut input_handler, scale_factor); - self.state.borrow_mut().input_handler = Some(input_handler); - result - } } #[inline] @@ -1288,6 +1304,7 @@ fn translate_message(handle: HWND, wparam: WPARAM, lparam: LPARAM) { unsafe { TranslateMessage(&msg).ok().log_err() }; } +#[profiling::function] fn handle_key_event( handle: HWND, wparam: WPARAM, @@ -1466,7 +1483,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 } + Capslock { on: on } } fn get_client_area_insets( @@ -1539,3 +1556,54 @@ fn notify_frame_changed(handle: HWND) { .log_err(); } } + +fn start_tracking_mouse( + handle: HWND, + state_ptr: &Rc, + flags: TRACKMOUSEEVENT_FLAGS, +) { + let mut lock = state_ptr.state.borrow_mut(); + if !lock.hovered { + lock.hovered = true; + unsafe { + TrackMouseEvent(&mut TRACKMOUSEEVENT { + cbSize: std::mem::size_of::() as u32, + dwFlags: flags, + hwndTrack: handle, + dwHoverTime: HOVER_DEFAULT, + }) + .log_err() + }; + if let Some(mut callback) = lock.callbacks.hovered_status_change.take() { + drop(lock); + callback(true); + state_ptr.state.borrow_mut().callbacks.hovered_status_change = Some(callback); + } + } +} + +fn with_input_handler(state_ptr: &Rc, f: F) -> Option +where + F: FnOnce(&mut PlatformInputHandler) -> R, +{ + let mut input_handler = state_ptr.state.borrow_mut().input_handler.take()?; + let result = f(&mut input_handler); + state_ptr.state.borrow_mut().input_handler = Some(input_handler); + Some(result) +} + +fn with_input_handler_and_scale_factor( + state_ptr: &Rc, + f: F, +) -> Option +where + F: FnOnce(&mut PlatformInputHandler, f32) -> Option, +{ + let mut lock = state_ptr.state.borrow_mut(); + let mut input_handler = lock.input_handler.take()?; + let scale_factor = lock.scale_factor; + drop(lock); + let result = f(&mut input_handler, scale_factor); + state_ptr.state.borrow_mut().input_handler = Some(input_handler); + result +} diff --git a/crates/gpui/src/platform/windows/keyboard.rs b/crates/gpui/src/platform/windows/keyboard.rs index 0eb97fbb0c..371feb70c2 100644 --- a/crates/gpui/src/platform/windows/keyboard.rs +++ b/crates/gpui/src/platform/windows/keyboard.rs @@ -1,31 +1,22 @@ use anyhow::Result; -use collections::HashMap; use windows::Win32::UI::{ Input::KeyboardAndMouse::{ - 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, + 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, }, WindowsAndMessaging::KL_NAMELENGTH, }; use windows_core::HSTRING; -use crate::{ - KeybindingKeystroke, Keystroke, Modifiers, PlatformKeyboardLayout, PlatformKeyboardMapper, -}; +use crate::{Modifiers, PlatformKeyboardLayout}; pub(crate) struct WindowsKeyboardLayout { id: String, name: String, } -pub(crate) struct WindowsKeyboardMapper { - key_to_vkey: HashMap, - vkey_to_key: HashMap, - vkey_to_shifted: HashMap, -} - impl PlatformKeyboardLayout for WindowsKeyboardLayout { fn id(&self) -> &str { &self.id @@ -36,65 +27,6 @@ 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 { - inner: keystroke, - display_modifiers: modifiers, - display_key: key, - } - } - - fn get_key_equivalents(&self) -> Option<&HashMap> { - None - } -} - impl WindowsKeyboardLayout { pub(crate) fn new() -> Result { let mut buffer = [0u16; KL_NAMELENGTH as usize]; @@ -116,41 +48,6 @@ 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, @@ -243,134 +140,3 @@ 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.display_key, "a"); - assert_eq!(mapped.display_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.display_key, "4"); - assert_eq!(mapped.display_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.display_key, "4"); - assert_eq!(mapped.display_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.display_key, "4"); - assert_eq!(mapped.display_modifiers, Modifiers::control_shift()); - } -} diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index 5ac2be2f23..5dfa631189 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -1,6 +1,5 @@ use std::{ cell::RefCell, - ffi::OsStr, mem::ManuallyDrop, path::{Path, PathBuf}, rc::Rc, @@ -15,18 +14,20 @@ use itertools::Itertools; use parking_lot::RwLock; use smallvec::SmallVec; use windows::{ - UI::ViewManagement::UISettings, - Win32::{ + core::*, Win32::{ Foundation::*, Graphics::{ + DirectComposition::DCompositionWaitForCompositorClock, + Dxgi::{ + CreateDXGIFactory2, IDXGIAdapter1, IDXGIFactory6, IDXGIOutput, DXGI_CREATE_FACTORY_FLAGS, DXGI_GPU_PREFERENCE_MINIMUM_POWER + }, Gdi::*, Imaging::{CLSID_WICImagingFactory, IWICImagingFactory}, }, Security::Credentials::*, System::{Com::*, LibraryLoader::*, Ole::*, SystemInformation::*, Threading::*}, UI::{Input::KeyboardAndMouse::*, Shell::*, WindowsAndMessaging::*}, - }, - core::*, + }, UI::ViewManagement::UISettings }; use crate::*; @@ -45,7 +46,6 @@ pub(crate) struct WindowsPlatform { drop_target_helper: IDropTargetHelper, validation_number: usize, main_thread_id_win32: u32, - disable_direct_composition: bool, } pub(crate) struct WindowsPlatformState { @@ -95,18 +95,14 @@ impl WindowsPlatform { main_thread_id_win32, 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) + DirectWriteTextSystem::new(&bitmap_factory) .context("Error creating DirectWriteTextSystem")?, ); let drop_target_helper: IDropTargetHelper = unsafe { @@ -126,7 +122,6 @@ impl WindowsPlatform { background_executor, foreground_executor, text_system, - disable_direct_composition, windows_version, bitmap_factory, drop_target_helper, @@ -135,12 +130,41 @@ impl WindowsPlatform { }) } - pub fn window_from_hwnd(&self, hwnd: HWND) -> Option> { + fn begin_vsync_thread(&self) { + let raw_window_handles = self.raw_window_handles.clone(); + std::thread::spawn(move || { + let vsync_provider = VSyncProvider::new(); + loop { + unsafe { + // DCompositionWaitForCompositorClock(None, INFINITE); + vsync_provider.wait_for_vsync(); + for handle in raw_window_handles.read().iter() { + RedrawWindow(Some(**handle), None, None, RDW_INVALIDATE) + .ok() + .log_err(); + // PostMessageW(Some(**handle), WM_GPUI_FORCE_DRAW_WINDOW, WPARAM(0), LPARAM(0)).log_err(); + } + } + } + }); + } + + fn redraw_all(&self) { + for handle in self.raw_window_handles.read().iter() { + unsafe { + RedrawWindow(Some(**handle), None, None, RDW_INVALIDATE | RDW_UPDATENOW) + .ok() + .log_err(); + } + } + } + + pub fn try_get_windows_inner_from_hwnd(&self, hwnd: HWND) -> Option> { self.raw_window_handles .read() .iter() - .find(|entry| entry.as_raw() == hwnd) - .and_then(|hwnd| window_from_hwnd(hwnd.as_raw())) + .find(|entry| ***entry == hwnd) + .and_then(|hwnd| try_get_window_inner(**hwnd)) } #[inline] @@ -149,7 +173,7 @@ impl WindowsPlatform { .read() .iter() .for_each(|handle| unsafe { - PostMessageW(Some(handle.as_raw()), message, wparam, lparam).log_err(); + PostMessageW(Some(**handle), message, wparam, lparam).log_err(); }); } @@ -157,7 +181,7 @@ impl WindowsPlatform { let mut lock = self.raw_window_handles.write(); let index = lock .iter() - .position(|handle| handle.as_raw() == target_window) + .position(|handle| **handle == target_window) .unwrap(); lock.remove(index); @@ -181,7 +205,6 @@ impl WindowsPlatform { validation_number: self.validation_number, main_receiver: self.main_receiver.clone(), main_thread_id_win32: self.main_thread_id_win32, - disable_direct_composition: self.disable_direct_composition, } } @@ -217,19 +240,20 @@ impl WindowsPlatform { } } - // Returns if the app should quit. - fn handle_events(&self) { + // Returns true if the app should quit. + fn handle_events(&self) -> bool { let mut msg = MSG::default(); unsafe { + // while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() { while GetMessageW(&mut msg, None, 0, 0).as_bool() { match msg.message { - WM_QUIT => return, + WM_QUIT => return true, WM_INPUTLANGCHANGE | WM_GPUI_CLOSE_ONE_WINDOW | WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD | WM_GPUI_DOCK_MENU_ACTION => { - if self.handle_gpui_events(msg.message, msg.wParam, msg.lParam, &msg) { - return; + if self.handle_gpui_evnets(msg.message, msg.wParam, msg.lParam, &msg) { + return true; } } _ => { @@ -238,10 +262,11 @@ impl WindowsPlatform { } } } + false } // Returns true if the app should quit. - fn handle_gpui_events( + fn handle_gpui_evnets( &self, message: u32, wparam: WPARAM, @@ -302,31 +327,17 @@ impl WindowsPlatform { if active_window_hwnd.is_invalid() { return None; } - self.raw_window_handles + if self + .raw_window_handles .read() .iter() - .find(|hwnd| hwnd.as_raw() == active_window_hwnd) - .map(|hwnd| hwnd.as_raw()) - } - - fn begin_vsync_thread(&self) { - let all_windows = Arc::downgrade(&self.raw_window_handles); - std::thread::spawn(move || { - let vsync_provider = VSyncProvider::new(); - loop { - vsync_provider.wait_for_vsync(); - 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(); - } - } - } - }); + .find(|&&hwnd| *hwnd == active_window_hwnd) + .is_some() + { + Some(active_window_hwnd) + } else { + None + } } } @@ -351,10 +362,6 @@ impl Platform for WindowsPlatform { ) } - fn keyboard_mapper(&self) -> Rc { - Rc::new(WindowsKeyboardMapper::new()) - } - fn on_keyboard_layout_change(&self, callback: Box) { self.state.borrow_mut().callbacks.keyboard_layout_change = Some(callback); } @@ -362,7 +369,12 @@ impl Platform for WindowsPlatform { fn run(&self, on_finish_launching: Box) { on_finish_launching(); self.begin_vsync_thread(); - self.handle_events(); + // loop { + if self.handle_events() { + // break; + } + // self.redraw_all(); + // } if let Some(ref mut callback) = self.state.borrow_mut().callbacks.quit { callback(); @@ -375,9 +387,9 @@ impl Platform for WindowsPlatform { .detach(); } - fn restart(&self, binary_path: Option) { + fn restart(&self, _: Option) { let pid = std::process::id(); - let Some(app_path) = binary_path.or(self.app_path().log_err()) else { + let Some(app_path) = self.app_path().log_err() else { return; }; let script = format!( @@ -444,7 +456,7 @@ impl Platform for WindowsPlatform { fn active_window(&self) -> Option { let active_window_hwnd = unsafe { GetActiveWindow() }; - self.window_from_hwnd(active_window_hwnd) + self.try_get_windows_inner_from_hwnd(active_window_hwnd) .map(|inner| inner.handle) } @@ -465,15 +477,13 @@ 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 { - open_target(&url_string) - .with_context(|| format!("Opening url: {}", url_string)) - .log_err(); + if url_string.is_empty() { + return; + } + open_target(url_string.as_str()); }) .detach(); } @@ -497,18 +507,13 @@ impl Platform for WindowsPlatform { rx } - fn prompt_for_new_path( - &self, - directory: &Path, - suggested_name: Option<&str>, - ) -> Receiver>> { + fn prompt_for_new_path(&self, directory: &Path) -> Receiver>> { 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, suggested_name, window)); + let _ = tx.send(file_save_dialog(directory, window)); }) .detach(); @@ -521,29 +526,37 @@ impl Platform for WindowsPlatform { } fn reveal_path(&self, path: &Path) { - if path.as_os_str().is_empty() { + let Ok(file_full_path) = path.canonicalize() else { + log::error!("unable to parse file path"); return; - } - let path = path.to_path_buf(); + }; self.background_executor() .spawn(async move { - open_target_in_explorer(&path) - .with_context(|| format!("Revealing path {} in explorer", path.display())) - .log_err(); + let Some(path) = file_full_path.to_str() else { + return; + }; + if path.is_empty() { + return; + } + open_target_in_explorer(path); }) .detach(); } fn open_with_system(&self, path: &Path) { - if path.as_os_str().is_empty() { + let Ok(full_path) = path.canonicalize() else { + log::error!("unable to parse file full path: {}", path.display()); return; - } - let path = path.to_path_buf(); + }; self.background_executor() .spawn(async move { - open_target(&path) - .with_context(|| format!("Opening {} with system", path.display())) - .log_err(); + let Some(full_path_str) = full_path.to_str() else { + return; + }; + if full_path_str.is_empty() { + return; + }; + open_target(full_path_str); }) .detach(); } @@ -731,70 +744,81 @@ pub(crate) struct WindowCreationInfo { pub(crate) validation_number: usize, pub(crate) main_receiver: flume::Receiver, pub(crate) main_thread_id_win32: u32, - pub(crate) disable_direct_composition: bool, } -fn open_target(target: impl AsRef) -> Result<()> { - let target = target.as_ref(); - let ret = unsafe { - ShellExecuteW( +struct VSyncProvider { + dxgi_output: IDXGIOutput, +} + +impl VSyncProvider { + fn new() -> Self { + let dxgi_factory: IDXGIFactory6 = + unsafe { CreateDXGIFactory2(DXGI_CREATE_FACTORY_FLAGS::default()) }.unwrap(); + let adapter: IDXGIAdapter1 = get_adapter(&dxgi_factory); + unsafe { + let dxgi_output = adapter.EnumOutputs(0).unwrap(); + Self { dxgi_output } + } + } + + fn wait_for_vsync(&self) { + unsafe { + self.dxgi_output.WaitForVBlank().unwrap(); + } + } +} + +fn get_adapter(dxgi_factory: &IDXGIFactory6) -> IDXGIAdapter1 { + unsafe { + for index in 0.. { + let adapter = dxgi_factory + .EnumAdapterByGpuPreference(index, DXGI_GPU_PREFERENCE_MINIMUM_POWER) + .unwrap(); + return adapter; + } + } + unreachable!("No DXGI adapter found") +} + +impl Default for VSyncProvider { + fn default() -> Self { + Self::new() + } +} + +fn open_target(target: &str) { + unsafe { + let ret = ShellExecuteW( None, windows::core::w!("open"), &HSTRING::from(target), None, None, SW_SHOWDEFAULT, - ) - }; - if ret.0 as isize <= 32 { - Err(anyhow::anyhow!( - "Unable to open target: {}", - std::io::Error::last_os_error() - )) - } else { - Ok(()) + ); + if ret.0 as isize <= 32 { + log::error!("Unable to open target: {}", std::io::Error::last_os_error()); + } } } -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(); +fn open_target_in_explorer(target: &str) { unsafe { - desktop.ParseDisplayName( - HWND::default(), + let ret = ShellExecuteW( None, - &HSTRING::from(dir), + windows::core::w!("open"), + windows::core::w!("explorer.exe"), + &HSTRING::from(format!("/select,{}", target).as_str()), None, - &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)) + SW_SHOWDEFAULT, + ); + if ret.0 as isize <= 32 { + log::error!( + "Unable to open target in explorer: {}", + std::io::Error::last_os_error() + ); } - }) + } } fn file_open_dialog( @@ -814,12 +838,6 @@ 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); @@ -842,26 +860,17 @@ fn file_open_dialog( Ok(Some(paths)) } -fn file_save_dialog( - directory: PathBuf, - suggested_name: Option, - window: Option, -) -> Result> { +fn file_save_dialog(directory: PathBuf, window: Option) -> Result> { let dialog: IFileSaveDialog = unsafe { CoCreateInstance(&FileSaveDialog, None, CLSCTX_ALL)? }; - if !directory.to_string_lossy().is_empty() - && 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() { + 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 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"), diff --git a/crates/gpui/src/platform/windows/shaders.hlsl b/crates/gpui/src/platform/windows/shaders.hlsl index 6fabe859e3..954040c4c3 100644 --- a/crates/gpui/src/platform/windows/shaders.hlsl +++ b/crates/gpui/src/platform/windows/shaders.hlsl @@ -1,6 +1,6 @@ cbuffer GlobalParams: register(b0) { float2 global_viewport_size; - uint2 _pad; + uint2 _global_pad; }; Texture2D t_sprite: register(t0); @@ -914,7 +914,7 @@ float4 path_rasterization_fragment(PathFragmentInput input): SV_Target { float2 dx = ddx(input.st_position); float2 dy = ddy(input.st_position); PathRasterizationSprite sprite = path_rasterization_sprites[input.vertex_id]; - + Background background = sprite.color; Bounds bounds = sprite.bounds; @@ -1021,18 +1021,13 @@ UnderlineVertexOutput underline_vertex(uint vertex_id: SV_VertexID, uint underli } float4 underline_fragment(UnderlineFragmentInput input): SV_Target { - const float WAVE_FREQUENCY = 2.0; - const float WAVE_HEIGHT_RATIO = 0.8; - Underline underline = underlines[input.underline_id]; if (underline.wavy) { float half_thickness = underline.thickness * 0.5; float2 origin = underline.bounds.origin; - float2 st = ((input.position.xy - origin) / underline.bounds.size.y) - float2(0., 0.5); - float frequency = (M_PI_F * WAVE_FREQUENCY * underline.thickness) / underline.bounds.size.y; - float amplitude = (underline.thickness * WAVE_HEIGHT_RATIO) / underline.bounds.size.y; - + float frequency = (M_PI_F * (3. * underline.thickness)) / 8.; + float amplitude = 1. / (2. * underline.thickness); float sine = sin(st.x * frequency) * amplitude; float dSine = cos(st.x * frequency) * amplitude * frequency; float distance = (st.y - sine) / sqrt(1. + dSine * dSine); @@ -1074,7 +1069,6 @@ struct MonochromeSpriteFragmentInput { float4 position: SV_Position; float2 tile_position: POSITION; nointerpolation float4 color: COLOR; - float4 clip_distance: SV_ClipDistance; }; StructuredBuffer mono_sprites: register(t1); @@ -1097,8 +1091,10 @@ 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); + float4 sample = t_sprite.Sample(s_sprite, input.tile_position); + float4 color = input.color; + color.a *= sample.a; + return color; } /* diff --git a/crates/gpui/src/platform/windows/util.rs b/crates/gpui/src/platform/windows/util.rs index af71dfe4a1..5fb8febe3b 100644 --- a/crates/gpui/src/platform/windows/util.rs +++ b/crates/gpui/src/platform/windows/util.rs @@ -1,18 +1,14 @@ use std::sync::OnceLock; use ::util::ResultExt; -use anyhow::Context; use windows::{ UI::{ Color, ViewManagement::{UIColorType, UISettings}, }, Wdk::System::SystemServices::RtlGetVersion, - Win32::{ - Foundation::*, Graphics::Dwm::*, System::LibraryLoader::LoadLibraryA, - UI::WindowsAndMessaging::*, - }, - core::{BOOL, HSTRING, PCSTR}, + Win32::{Foundation::*, Graphics::Dwm::*, UI::WindowsAndMessaging::*}, + core::{BOOL, HSTRING}, }; use crate::*; @@ -201,19 +197,3 @@ pub(crate) fn show_error(title: &str, content: String) { ) }; } - -pub(crate) fn with_dll_library(dll_name: PCSTR, f: F) -> Result -where - F: FnOnce(HMODULE) -> Result, -{ - let library = unsafe { - LoadLibraryA(dll_name).with_context(|| format!("Loading dll: {}", dll_name.display()))? - }; - let result = f(library); - unsafe { - FreeLibrary(library) - .with_context(|| format!("Freeing dll: {}", dll_name.display())) - .log_err(); - } - result -} diff --git a/crates/gpui/src/platform/windows/vsync.rs b/crates/gpui/src/platform/windows/vsync.rs deleted file mode 100644 index 6d09b0960f..0000000000 --- a/crates/gpui/src/platform/windows/vsync.rs +++ /dev/null @@ -1,174 +0,0 @@ -use std::{ - sync::LazyLock, - time::{Duration, Instant}, -}; - -use anyhow::{Context, Result}; -use util::ResultExt; -use windows::{ - Win32::{ - Foundation::{HANDLE, HWND}, - Graphics::{ - DirectComposition::{ - COMPOSITION_FRAME_ID_COMPLETED, COMPOSITION_FRAME_ID_TYPE, COMPOSITION_FRAME_STATS, - COMPOSITION_TARGET_ID, - }, - Dwm::{DWM_TIMING_INFO, DwmFlush, DwmGetCompositionTimingInfo}, - }, - System::{ - LibraryLoader::{GetModuleHandleA, GetProcAddress}, - Performance::QueryPerformanceFrequency, - Threading::INFINITE, - }, - }, - core::{HRESULT, s}, -}; - -static QPC_TICKS_PER_SECOND: LazyLock = LazyLock::new(|| { - let mut frequency = 0; - // On systems that run Windows XP or later, the function will always succeed and - // will thus never return zero. - unsafe { QueryPerformanceFrequency(&mut frequency).unwrap() }; - frequency as u64 -}); - -const VSYNC_INTERVAL_THRESHOLD: Duration = Duration::from_millis(1); -const DEFAULT_VSYNC_INTERVAL: Duration = Duration::from_micros(16_666); // ~60Hz - -// Here we are using dynamic loading of DirectComposition functions, -// or the app will refuse to start on windows systems that do not support DirectComposition. -type DCompositionGetFrameId = - unsafe extern "system" fn(frameidtype: COMPOSITION_FRAME_ID_TYPE, frameid: *mut u64) -> HRESULT; -type DCompositionGetStatistics = unsafe extern "system" fn( - frameid: u64, - framestats: *mut COMPOSITION_FRAME_STATS, - targetidcount: u32, - targetids: *mut COMPOSITION_TARGET_ID, - actualtargetidcount: *mut u32, -) -> HRESULT; -type DCompositionWaitForCompositorClock = - unsafe extern "system" fn(count: u32, handles: *const HANDLE, timeoutinms: u32) -> u32; - -pub(crate) struct VSyncProvider { - interval: Duration, - f: Box bool>, -} - -impl VSyncProvider { - pub(crate) fn new() -> Self { - if let Some((get_frame_id, get_statistics, wait_for_comp_clock)) = - initialize_direct_composition() - .context("Retrieving DirectComposition functions") - .log_with_level(log::Level::Warn) - { - let interval = get_dwm_interval_from_direct_composition(get_frame_id, get_statistics) - .context("Failed to get DWM interval from DirectComposition") - .log_err() - .unwrap_or(DEFAULT_VSYNC_INTERVAL); - log::info!( - "DirectComposition is supported for VSync, interval: {:?}", - interval - ); - let f = Box::new(move || unsafe { - wait_for_comp_clock(0, std::ptr::null(), INFINITE) == 0 - }); - Self { interval, f } - } else { - let interval = get_dwm_interval() - .context("Failed to get DWM interval") - .log_err() - .unwrap_or(DEFAULT_VSYNC_INTERVAL); - log::info!( - "DirectComposition is not supported for VSync, falling back to DWM, interval: {:?}", - interval - ); - let f = Box::new(|| unsafe { DwmFlush().is_ok() }); - Self { interval, f } - } - } - - pub(crate) fn wait_for_vsync(&self) { - let vsync_start = Instant::now(); - let wait_succeeded = (self.f)(); - let elapsed = vsync_start.elapsed(); - // 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 - // 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. - if !wait_succeeded || elapsed < VSYNC_INTERVAL_THRESHOLD { - log::trace!("VSyncProvider::wait_for_vsync() took less time than expected"); - std::thread::sleep(self.interval); - } - } -} - -fn initialize_direct_composition() -> Result<( - DCompositionGetFrameId, - DCompositionGetStatistics, - DCompositionWaitForCompositorClock, -)> { - unsafe { - // Load DLL at runtime since older Windows versions don't have dcomp. - let hmodule = GetModuleHandleA(s!("dcomp.dll")).context("Loading dcomp.dll")?; - let get_frame_id_addr = GetProcAddress(hmodule, s!("DCompositionGetFrameId")) - .context("Function DCompositionGetFrameId not found")?; - let get_statistics_addr = GetProcAddress(hmodule, s!("DCompositionGetStatistics")) - .context("Function DCompositionGetStatistics not found")?; - let wait_for_compositor_clock_addr = - GetProcAddress(hmodule, s!("DCompositionWaitForCompositorClock")) - .context("Function DCompositionWaitForCompositorClock not found")?; - let get_frame_id: DCompositionGetFrameId = std::mem::transmute(get_frame_id_addr); - let get_statistics: DCompositionGetStatistics = std::mem::transmute(get_statistics_addr); - let wait_for_compositor_clock: DCompositionWaitForCompositorClock = - std::mem::transmute(wait_for_compositor_clock_addr); - Ok((get_frame_id, get_statistics, wait_for_compositor_clock)) - } -} - -fn get_dwm_interval_from_direct_composition( - get_frame_id: DCompositionGetFrameId, - get_statistics: DCompositionGetStatistics, -) -> Result { - let mut frame_id = 0; - unsafe { get_frame_id(COMPOSITION_FRAME_ID_COMPLETED, &mut frame_id) }.ok()?; - let mut stats = COMPOSITION_FRAME_STATS::default(); - unsafe { - get_statistics( - frame_id, - &mut stats, - 0, - std::ptr::null_mut(), - std::ptr::null_mut(), - ) - } - .ok()?; - Ok(retrieve_duration(stats.framePeriod, *QPC_TICKS_PER_SECOND)) -} - -fn get_dwm_interval() -> Result { - let mut timing_info = DWM_TIMING_INFO { - cbSize: std::mem::size_of::() as u32, - ..Default::default() - }; - unsafe { DwmGetCompositionTimingInfo(HWND::default(), &mut timing_info) }?; - let interval = retrieve_duration(timing_info.qpcRefreshPeriod, *QPC_TICKS_PER_SECOND); - // Check for interval values that are impossibly low. A 29 microsecond - // interval was seen (from a qpcRefreshPeriod of 60). - if interval < VSYNC_INTERVAL_THRESHOLD { - Ok(retrieve_duration( - timing_info.rateRefresh.uiDenominator as u64, - timing_info.rateRefresh.uiNumerator as u64, - )) - } else { - Ok(interval) - } -} - -#[inline] -fn retrieve_duration(counts: u64, ticks_per_second: u64) -> Duration { - let ticks_per_microsecond = ticks_per_second / 1_000_000; - Duration::from_micros(counts / ticks_per_microsecond) -} diff --git a/crates/gpui/src/platform/windows/window.rs b/crates/gpui/src/platform/windows/window.rs index 99e5073371..42f6847a23 100644 --- a/crates/gpui/src/platform/windows/window.rs +++ b/crates/gpui/src/platform/windows/window.rs @@ -28,7 +28,7 @@ use windows::{ use crate::*; -pub(crate) struct WindowsWindow(pub Rc); +pub(crate) struct WindowsWindow(pub Rc); pub struct WindowsWindowState { pub origin: Point, @@ -49,6 +49,7 @@ pub struct WindowsWindowState { pub hovered: bool, pub renderer: DirectXRenderer, + pub keydown_time: Option, pub click_state: ClickState, pub system_settings: WindowsSystemSettings, @@ -61,9 +62,9 @@ pub struct WindowsWindowState { hwnd: HWND, } -pub(crate) struct WindowsWindowInner { +pub(crate) struct WindowsWindowStatePtr { hwnd: HWND, - pub(super) this: Weak, + this: Weak, drop_target_helper: IDropTargetHelper, pub(crate) state: RefCell, pub(crate) handle: AnyWindowHandle, @@ -79,7 +80,7 @@ pub(crate) struct WindowsWindowInner { impl WindowsWindowState { fn new( hwnd: HWND, - window_params: &CREATESTRUCTW, + cs: &CREATESTRUCTW, current_cursor: Option, display: WindowsDisplay, min_size: Option>, @@ -90,12 +91,9 @@ impl WindowsWindowState { let monitor_dpi = unsafe { GetDpiForWindow(hwnd) } as f32; monitor_dpi / USER_DEFAULT_SCREEN_DPI as f32 }; - let origin = logical_point(window_params.x as f32, window_params.y as f32, scale_factor); + let origin = logical_point(cs.x as f32, cs.y as f32, scale_factor); let logical_size = { - let physical_size = size( - DevicePixels(window_params.cx), - DevicePixels(window_params.cy), - ); + let physical_size = size(DevicePixels(cs.cx), DevicePixels(cs.cy)); physical_size.to_pixels(scale_factor) }; let fullscreen_restore_bounds = Bounds { @@ -118,6 +116,7 @@ impl WindowsWindowState { let nc_button_pressed = None; let fullscreen = None; let initial_placement = None; + let keydown_time = None; Ok(Self { origin, @@ -136,6 +135,7 @@ impl WindowsWindowState { system_key_handled, hovered, renderer, + keydown_time, click_state, system_settings, current_cursor, @@ -204,7 +204,7 @@ impl WindowsWindowState { } } -impl WindowsWindowInner { +impl WindowsWindowStatePtr { fn new(context: &WindowCreateContext, hwnd: HWND, cs: &CREATESTRUCTW) -> Result> { let state = RefCell::new(WindowsWindowState::new( hwnd, @@ -233,13 +233,13 @@ impl WindowsWindowInner { } fn toggle_fullscreen(&self) { - let Some(this) = self.this.upgrade() else { + let Some(state_ptr) = self.this.upgrade() else { log::error!("Unable to toggle fullscreen: window has been dropped"); return; }; self.executor .spawn(async move { - let mut lock = this.state.borrow_mut(); + let mut lock = state_ptr.state.borrow_mut(); let StyleAndBounds { style, x, @@ -251,9 +251,10 @@ impl WindowsWindowInner { } else { let (window_bounds, _) = lock.calculate_window_bounds(); lock.fullscreen_restore_bounds = window_bounds; - let style = WINDOW_STYLE(unsafe { get_window_long(this.hwnd, GWL_STYLE) } as _); + let style = + WINDOW_STYLE(unsafe { get_window_long(state_ptr.hwnd, GWL_STYLE) } as _); let mut rc = RECT::default(); - unsafe { GetWindowRect(this.hwnd, &mut rc) }.log_err(); + unsafe { GetWindowRect(state_ptr.hwnd, &mut rc) }.log_err(); let _ = lock.fullscreen.insert(StyleAndBounds { style, x: rc.left, @@ -277,10 +278,10 @@ impl WindowsWindowInner { } }; drop(lock); - unsafe { set_window_long(this.hwnd, GWL_STYLE, style.0 as isize) }; + unsafe { set_window_long(state_ptr.hwnd, GWL_STYLE, style.0 as isize) }; unsafe { SetWindowPos( - this.hwnd, + state_ptr.hwnd, None, x, y, @@ -330,7 +331,7 @@ pub(crate) struct Callbacks { } struct WindowCreateContext { - inner: Option>>, + inner: Option>>, handle: AnyWindowHandle, hide_title_bar: bool, display: WindowsDisplay, @@ -362,15 +363,14 @@ impl WindowsWindow { validation_number, main_receiver, main_thread_id_win32, - disable_direct_composition, } = creation_info; - register_window_class(icon); + let classname = register_wnd_class(icon); let hide_title_bar = params .titlebar .as_ref() .map(|titlebar| titlebar.appears_transparent) .unwrap_or(true); - let window_name = HSTRING::from( + let windowname = HSTRING::from( params .titlebar .as_ref() @@ -378,6 +378,8 @@ impl WindowsWindow { .map(|title| title.as_ref()) .unwrap_or(""), ); + let disable_direct_composition = std::env::var(DISABLE_DIRECT_COMPOSITION) + .is_ok_and(|value| value == "true" || value == "1"); let (mut dwexstyle, dwstyle) = if params.kind == WindowKind::PopUp { (WS_EX_TOOLWINDOW, WINDOW_STYLE(0x0)) @@ -416,11 +418,12 @@ impl WindowsWindow { appearance, disable_direct_composition, }; + let lpparam = Some(&context as *const _ as *const _); let creation_result = unsafe { CreateWindowExW( dwexstyle, - WINDOW_CLASS_NAME, - &window_name, + classname, + &windowname, dwstyle, CW_USEDEFAULT, CW_USEDEFAULT, @@ -429,35 +432,33 @@ impl WindowsWindow { None, None, Some(hinstance.into()), - Some(&context as *const _ as *const _), + lpparam, ) }; - - // Failure to create a `WindowsWindowState` can cause window creation to fail, - // so check the inner result first. - let this = context.inner.take().unwrap()?; + // We should call `?` on state_ptr first, then call `?` on hwnd. + // Or, we will lose the error info reported by `WindowsWindowState::new` + let state_ptr = context.inner.take().unwrap()?; let hwnd = creation_result?; - - register_drag_drop(&this)?; + register_drag_drop(state_ptr.clone())?; configure_dwm_dark_mode(hwnd, appearance); - this.state.borrow_mut().border_offset.update(hwnd)?; + state_ptr.state.borrow_mut().border_offset.update(hwnd)?; let placement = retrieve_window_placement( hwnd, display, params.bounds, - this.state.borrow().scale_factor, - this.state.borrow().border_offset, + state_ptr.state.borrow().scale_factor, + state_ptr.state.borrow().border_offset, )?; if params.show { unsafe { SetWindowPlacement(hwnd, &placement)? }; } else { - this.state.borrow_mut().initial_placement = Some(WindowOpenStatus { + state_ptr.state.borrow_mut().initial_placement = Some(WindowOpenStatus { placement, state: WindowOpenState::Windowed, }); } - Ok(Self(this)) + Ok(Self(state_ptr)) } } @@ -592,7 +593,10 @@ impl PlatformWindow for WindowsWindow { ) -> Option> { let (done_tx, done_rx) = oneshot::channel(); let msg = msg.to_string(); - let detail_string = detail.map(|detail| detail.to_string()); + let detail_string = match detail { + Some(info) => Some(info.to_string()), + None => None, + }; let handle = self.0.hwnd; let answers = answers.to_vec(); self.0 @@ -674,36 +678,6 @@ impl PlatformWindow for WindowsWindow { this.set_window_placement().log_err(); unsafe { SetActiveWindow(hwnd).log_err() }; unsafe { SetFocus(Some(hwnd)).log_err() }; - - // premium ragebait by windows, this is needed because the window - // must have received an input event to be able to set itself to foreground - // so let's just simulate user input as that seems to be the most reliable way - // some more info: https://gist.github.com/Aetopia/1581b40f00cc0cadc93a0e8ccb65dc8c - // bonus: this bug also doesn't manifest if you have vs attached to the process - let inputs = [ - INPUT { - r#type: INPUT_KEYBOARD, - Anonymous: INPUT_0 { - ki: KEYBDINPUT { - wVk: VK_MENU, - dwFlags: KEYBD_EVENT_FLAGS(0), - ..Default::default() - }, - }, - }, - INPUT { - r#type: INPUT_KEYBOARD, - Anonymous: INPUT_0 { - ki: KEYBDINPUT { - wVk: VK_MENU, - dwFlags: KEYEVENTF_KEYUP, - ..Default::default() - }, - }, - }, - ]; - unsafe { SendInput(&inputs, std::mem::size_of::() as i32) }; - // todo(windows) // crate `windows 0.56` reports true as Err unsafe { SetForegroundWindow(hwnd).as_bool() }; @@ -833,7 +807,7 @@ impl PlatformWindow for WindowsWindow { } #[implement(IDropTarget)] -struct WindowsDragDropHandler(pub Rc); +struct WindowsDragDropHandler(pub Rc); impl WindowsDragDropHandler { fn handle_drag_drop(&self, input: PlatformInput) { @@ -1114,15 +1088,15 @@ enum WindowOpenState { Windowed, } -const WINDOW_CLASS_NAME: PCWSTR = w!("Zed::Window"); +fn register_wnd_class(icon_handle: HICON) -> PCWSTR { + const CLASS_NAME: PCWSTR = w!("Zed::Window"); -fn register_window_class(icon_handle: HICON) { static ONCE: Once = Once::new(); ONCE.call_once(|| { let wc = WNDCLASSW { - lpfnWndProc: Some(window_procedure), + lpfnWndProc: Some(wnd_proc), hIcon: icon_handle, - lpszClassName: PCWSTR(WINDOW_CLASS_NAME.as_ptr()), + lpszClassName: PCWSTR(CLASS_NAME.as_ptr()), style: CS_HREDRAW | CS_VREDRAW, hInstance: get_module_handle().into(), hbrBackground: unsafe { CreateSolidBrush(COLORREF(0x00000000)) }, @@ -1130,58 +1104,54 @@ fn register_window_class(icon_handle: HICON) { }; unsafe { RegisterClassW(&wc) }; }); + + CLASS_NAME } -unsafe extern "system" fn window_procedure( +unsafe extern "system" fn wnd_proc( hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM, ) -> LRESULT { if msg == WM_NCCREATE { - let window_params = lparam.0 as *const CREATESTRUCTW; - let window_params = unsafe { &*window_params }; - let window_creation_context = window_params.lpCreateParams as *mut WindowCreateContext; - let window_creation_context = unsafe { &mut *window_creation_context }; - return match WindowsWindowInner::new(window_creation_context, hwnd, window_params) { - Ok(window_state) => { - let weak = Box::new(Rc::downgrade(&window_state)); - unsafe { set_window_long(hwnd, GWLP_USERDATA, Box::into_raw(weak) as isize) }; - window_creation_context.inner = Some(Ok(window_state)); - unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) } - } - Err(error) => { - window_creation_context.inner = Some(Err(error)); - LRESULT(0) - } - }; + let cs = lparam.0 as *const CREATESTRUCTW; + let cs = unsafe { &*cs }; + let ctx = cs.lpCreateParams as *mut WindowCreateContext; + let ctx = unsafe { &mut *ctx }; + let creation_result = WindowsWindowStatePtr::new(ctx, hwnd, cs); + if creation_result.is_err() { + ctx.inner = Some(creation_result); + return LRESULT(0); + } + let weak = Box::new(Rc::downgrade(creation_result.as_ref().unwrap())); + unsafe { set_window_long(hwnd, GWLP_USERDATA, Box::into_raw(weak) as isize) }; + ctx.inner = Some(creation_result); + return unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }; } - - let ptr = unsafe { get_window_long(hwnd, GWLP_USERDATA) } as *mut Weak; + let ptr = unsafe { get_window_long(hwnd, GWLP_USERDATA) } as *mut Weak; 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) + let r = if let Some(state) = inner.upgrade() { + handle_msg(hwnd, msg, wparam, lparam, state) } 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 + r } -pub(crate) fn window_from_hwnd(hwnd: HWND) -> Option> { +pub(crate) fn try_get_window_inner(hwnd: HWND) -> Option> { if hwnd.is_invalid() { return None; } - let ptr = unsafe { get_window_long(hwnd, GWLP_USERDATA) } as *mut Weak; + let ptr = unsafe { get_window_long(hwnd, GWLP_USERDATA) } as *mut Weak; if !ptr.is_null() { let inner = unsafe { &*ptr }; inner.upgrade() @@ -1204,9 +1174,9 @@ fn get_module_handle() -> HMODULE { } } -fn register_drag_drop(window: &Rc) -> Result<()> { - let window_handle = window.hwnd; - let handler = WindowsDragDropHandler(window.clone()); +fn register_drag_drop(state_ptr: Rc) -> Result<()> { + let window_handle = state_ptr.hwnd; + let handler = WindowsDragDropHandler(state_ptr); // The lifetime of `IDropTarget` is handled by Windows, it won't release until // we call `RevokeDragDrop`. // So, it's safe to drop it here. diff --git a/crates/gpui/src/platform/windows/wrapper.rs b/crates/gpui/src/platform/windows/wrapper.rs index 60bbc433ca..9d9df3a796 100644 --- a/crates/gpui/src/platform/windows/wrapper.rs +++ b/crates/gpui/src/platform/windows/wrapper.rs @@ -1,6 +1,31 @@ use std::ops::Deref; -use windows::Win32::{Foundation::HWND, UI::WindowsAndMessaging::HCURSOR}; +use windows::Win32::{ + Foundation::{HANDLE, HWND}, + UI::WindowsAndMessaging::HCURSOR, +}; + +#[derive(Debug, Clone, Copy)] +pub(crate) struct SafeHandle { + raw: HANDLE, +} + +unsafe impl Send for SafeHandle {} +unsafe impl Sync for SafeHandle {} + +impl From for SafeHandle { + fn from(value: HANDLE) -> Self { + SafeHandle { raw: value } + } +} + +impl Deref for SafeHandle { + type Target = HANDLE; + + fn deref(&self) -> &Self::Target { + &self.raw + } +} #[derive(Debug, Clone, Copy)] pub(crate) struct SafeCursor { @@ -24,17 +49,11 @@ impl Deref for SafeCursor { } } -#[derive(Debug, Clone, Copy)] +#[derive(Clone, Copy)] pub(crate) struct SafeHwnd { raw: HWND, } -impl SafeHwnd { - pub(crate) fn as_raw(&self) -> HWND { - self.raw - } -} - unsafe impl Send for SafeHwnd {} unsafe impl Sync for SafeHwnd {} @@ -44,6 +63,12 @@ impl From for SafeHwnd { } } +impl From for HWND { + fn from(value: SafeHwnd) -> Self { + value.raw + } +} + impl Deref for SafeHwnd { type Target = HWND; diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index 758d06e597..ec8d720cdf 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -8,12 +8,7 @@ use crate::{ AtlasTextureId, AtlasTile, Background, Bounds, ContentMask, Corners, Edges, Hsla, Pixels, Point, Radians, ScaledPixels, Size, bounds_tree::BoundsTree, point, }; -use std::{ - fmt::Debug, - iter::Peekable, - ops::{Add, Range, Sub}, - slice, -}; +use std::{fmt::Debug, iter::Peekable, ops::Range, slice}; #[allow(non_camel_case_types, unused)] pub(crate) type PathVertex_ScaledPixels = PathVertex; @@ -476,7 +471,7 @@ pub(crate) struct Underline { pub content_mask: ContentMask, pub color: Hsla, pub thickness: ScaledPixels, - pub wavy: u32, + pub wavy: bool, } impl From for Primitive { @@ -798,16 +793,6 @@ impl Path { } } -impl Path -where - T: Clone + Debug + Default + PartialEq + PartialOrd + Add + Sub, -{ - #[allow(unused)] - pub(crate) fn clipped_bounds(&self) -> Bounds { - self.bounds.intersect(&self.content_mask.bounds) - } -} - impl From> for Primitive { fn from(path: Path) -> Self { Primitive::Path(path) diff --git a/crates/gpui/src/shared_string.rs b/crates/gpui/src/shared_string.rs index 350184d350..c325f98cd2 100644 --- a/crates/gpui/src/shared_string.rs +++ b/crates/gpui/src/shared_string.rs @@ -23,11 +23,6 @@ impl SharedString { pub fn new(str: impl Into>) -> 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 { @@ -108,7 +103,7 @@ impl From for Arc { fn from(val: SharedString) -> Self { match val.0 { ArcCow::Borrowed(borrowed) => Arc::from(borrowed), - ArcCow::Owned(owned) => owned, + ArcCow::Owned(owned) => owned.clone(), } } } diff --git a/crates/gpui/src/style.rs b/crates/gpui/src/style.rs index 5b69ce7fa6..560de7b924 100644 --- a/crates/gpui/src/style.rs +++ b/crates/gpui/src/style.rs @@ -7,7 +7,7 @@ use std::{ use crate::{ AbsoluteLength, App, Background, BackgroundTag, BorderStyle, Bounds, ContentMask, Corners, CornersRefinement, CursorStyle, DefiniteLength, DevicePixels, Edges, EdgesRefinement, Font, - FontFallbacks, FontFeatures, FontStyle, FontWeight, GridLocation, Hsla, Length, Pixels, Point, + FontFallbacks, FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba, SharedString, Size, SizeRefinement, Styled, TextRun, Window, black, phi, point, quad, rems, size, }; @@ -260,17 +260,6 @@ pub struct Style { /// The opacity of this element pub opacity: Option, - /// The grid columns of this element - /// Equivalent to the Tailwind `grid-cols-` - pub grid_cols: Option, - - /// The row span of this element - /// Equivalent to the Tailwind `grid-rows-` - pub grid_rows: Option, - - /// The grid location of this element - pub grid_location: Option, - /// Whether to draw a red debugging outline around this element #[cfg(debug_assertions)] pub debug: bool, @@ -286,13 +275,6 @@ impl Styled for StyleRefinement { } } -impl StyleRefinement { - /// The grid location of this element - pub fn grid_location_mut(&mut self) -> &mut GridLocation { - self.grid_location.get_or_insert_default() - } -} - /// The value of the visibility property, similar to the CSS property `visibility` #[derive(Default, Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)] pub enum Visibility { @@ -573,7 +555,7 @@ impl Style { if self .border_color - .is_some_and(|color| !color.is_transparent()) + .map_or(false, |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 +615,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.is_some_and(|color| !color.is_transparent()) { + if background_color.map_or(false, |color| !color.is_transparent()) { let mut border_color = match background_color { Some(color) => match color.tag { BackgroundTag::Solid => color.solid, @@ -729,7 +711,7 @@ impl Style { fn is_border_visible(&self) -> bool { self.border_color - .is_some_and(|color| !color.is_transparent()) + .map_or(false, |color| !color.is_transparent()) && self.border_widths.any(|length| !length.is_zero()) } } @@ -775,9 +757,6 @@ impl Default for Style { text: TextStyleRefinement::default(), mouse_cursor: None, opacity: None, - grid_rows: None, - grid_cols: None, - grid_location: None, #[cfg(debug_assertions)] debug: false, diff --git a/crates/gpui/src/styled.rs b/crates/gpui/src/styled.rs index c714cac14f..b689f32687 100644 --- a/crates/gpui/src/styled.rs +++ b/crates/gpui/src/styled.rs @@ -1,8 +1,8 @@ use crate::{ self as gpui, AbsoluteLength, AlignContent, AlignItems, BorderStyle, CursorStyle, - DefiniteLength, Display, Fill, FlexDirection, FlexWrap, Font, FontStyle, FontWeight, - GridPlacement, Hsla, JustifyContent, Length, SharedString, StrikethroughStyle, StyleRefinement, - TextAlign, TextOverflow, TextStyleRefinement, UnderlineStyle, WhiteSpace, px, relative, rems, + DefiniteLength, Display, Fill, FlexDirection, FlexWrap, Font, FontStyle, FontWeight, Hsla, + JustifyContent, Length, SharedString, StrikethroughStyle, StyleRefinement, TextAlign, + TextOverflow, TextStyleRefinement, UnderlineStyle, WhiteSpace, px, relative, rems, }; pub use gpui_macros::{ border_style_methods, box_shadow_style_methods, cursor_style_methods, margin_style_methods, @@ -46,13 +46,6 @@ pub trait Styled: Sized { self } - /// Sets the display type of the element to `grid`. - /// [Docs](https://tailwindcss.com/docs/display) - fn grid(mut self) -> Self { - self.style().display = Some(Display::Grid); - self - } - /// Sets the whitespace of the element to `normal`. /// [Docs](https://tailwindcss.com/docs/whitespace#normal) fn whitespace_normal(mut self) -> Self { @@ -647,102 +640,6 @@ pub trait Styled: Sized { self } - /// Sets the grid columns of this element. - fn grid_cols(mut self, cols: u16) -> Self { - self.style().grid_cols = Some(cols); - self - } - - /// Sets the grid rows of this element. - fn grid_rows(mut self, rows: u16) -> Self { - self.style().grid_rows = Some(rows); - self - } - - /// Sets the column start of this element. - fn col_start(mut self, start: i16) -> Self { - let grid_location = self.style().grid_location_mut(); - grid_location.column.start = GridPlacement::Line(start); - self - } - - /// Sets the column start of this element to auto. - fn col_start_auto(mut self) -> Self { - let grid_location = self.style().grid_location_mut(); - grid_location.column.start = GridPlacement::Auto; - self - } - - /// Sets the column end of this element. - fn col_end(mut self, end: i16) -> Self { - let grid_location = self.style().grid_location_mut(); - grid_location.column.end = GridPlacement::Line(end); - self - } - - /// Sets the column end of this element to auto. - fn col_end_auto(mut self) -> Self { - let grid_location = self.style().grid_location_mut(); - grid_location.column.end = GridPlacement::Auto; - self - } - - /// Sets the column span of this element. - fn col_span(mut self, span: u16) -> Self { - let grid_location = self.style().grid_location_mut(); - grid_location.column = GridPlacement::Span(span)..GridPlacement::Span(span); - self - } - - /// Sets the row span of this element. - fn col_span_full(mut self) -> Self { - let grid_location = self.style().grid_location_mut(); - grid_location.column = GridPlacement::Line(1)..GridPlacement::Line(-1); - self - } - - /// Sets the row start of this element. - fn row_start(mut self, start: i16) -> Self { - let grid_location = self.style().grid_location_mut(); - grid_location.row.start = GridPlacement::Line(start); - self - } - - /// Sets the row start of this element to "auto" - fn row_start_auto(mut self) -> Self { - let grid_location = self.style().grid_location_mut(); - grid_location.row.start = GridPlacement::Auto; - self - } - - /// Sets the row end of this element. - fn row_end(mut self, end: i16) -> Self { - let grid_location = self.style().grid_location_mut(); - grid_location.row.end = GridPlacement::Line(end); - self - } - - /// Sets the row end of this element to "auto" - fn row_end_auto(mut self) -> Self { - let grid_location = self.style().grid_location_mut(); - grid_location.row.end = GridPlacement::Auto; - self - } - - /// Sets the row span of this element. - fn row_span(mut self, span: u16) -> Self { - let grid_location = self.style().grid_location_mut(); - grid_location.row = GridPlacement::Span(span)..GridPlacement::Span(span); - self - } - - /// Sets the row span of this element. - fn row_span_full(mut self) -> Self { - let grid_location = self.style().grid_location_mut(); - grid_location.row = GridPlacement::Line(1)..GridPlacement::Line(-1); - self - } - /// Draws a debug border around this element. #[cfg(debug_assertions)] fn debug(mut self) -> Self { diff --git a/crates/gpui/src/subscription.rs b/crates/gpui/src/subscription.rs index bd869f8d32..a584f1a45f 100644 --- a/crates/gpui/src/subscription.rs +++ b/crates/gpui/src/subscription.rs @@ -201,9 +201,3 @@ 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() - } -} diff --git a/crates/gpui/src/tab_stop.rs b/crates/gpui/src/tab_stop.rs index c4d2fda6e9..7dde42efed 100644 --- a/crates/gpui/src/tab_stop.rs +++ b/crates/gpui/src/tab_stop.rs @@ -45,18 +45,27 @@ impl TabHandles { }) .unwrap_or_default(); - self.handles.get(next_ix).cloned() + if let Some(next_handle) = self.handles.get(next_ix) { + Some(next_handle.clone()) + } else { + None + } } pub(crate) fn prev(&self, focused_id: Option<&FocusId>) -> Option { let ix = self.current_index(focused_id).unwrap_or_default(); - let prev_ix = if ix == 0 { - self.handles.len().saturating_sub(1) + let prev_ix; + if ix == 0 { + prev_ix = self.handles.len().saturating_sub(1); } else { - ix.saturating_sub(1) - }; + prev_ix = ix.saturating_sub(1); + } - self.handles.get(prev_ix).cloned() + if let Some(prev_handle) = self.handles.get(prev_ix) { + Some(prev_handle.clone()) + } else { + None + } } } @@ -81,7 +90,7 @@ mod tests { ]; for handle in focus_handles.iter() { - tab.insert(handle); + tab.insert(&handle); } assert_eq!( tab.handles diff --git a/crates/gpui/src/taffy.rs b/crates/gpui/src/taffy.rs index 58386ad1f5..f7fa54256d 100644 --- a/crates/gpui/src/taffy.rs +++ b/crates/gpui/src/taffy.rs @@ -3,8 +3,7 @@ use crate::{ }; use collections::{FxHashMap, FxHashSet}; use smallvec::SmallVec; -use stacksafe::{StackSafe, stacksafe}; -use std::{fmt::Debug, ops::Range}; +use std::fmt::Debug; use taffy::{ TaffyTree, TraversePartialTree as _, geometry::{Point as TaffyPoint, Rect as TaffyRect, Size as TaffySize}, @@ -12,15 +11,8 @@ use taffy::{ tree::NodeId, }; -type NodeMeasureFn = StackSafe< - Box< - dyn FnMut( - Size>, - Size, - &mut Window, - &mut App, - ) -> Size, - >, +type NodeMeasureFn = Box< + dyn FnMut(Size>, Size, &mut Window, &mut App) -> Size, >; struct NodeContext { @@ -58,21 +50,23 @@ impl TaffyLayoutEngine { children: &[LayoutId], ) -> LayoutId { let taffy_style = style.to_taffy(rem_size); - - if children.is_empty() { + let layout_id = if children.is_empty() { self.taffy .new_leaf(taffy_style) .expect(EXPECT_MESSAGE) .into() } else { - self.taffy + let parent_id = 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() - } + .into(); + parent_id + }; + layout_id } pub fn request_measured_layout( @@ -89,15 +83,17 @@ impl TaffyLayoutEngine { ) -> LayoutId { let taffy_style = style.to_taffy(rem_size); - self.taffy + let layout_id = self + .taffy .new_leaf_with_context( taffy_style, NodeContext { - measure: StackSafe::new(Box::new(measure)), + measure: Box::new(measure), }, ) .expect(EXPECT_MESSAGE) - .into() + .into(); + layout_id } // Used to understand performance @@ -147,7 +143,6 @@ impl TaffyLayoutEngine { Ok(edges) } - #[stacksafe] pub fn compute_layout( &mut self, id: LayoutId, @@ -164,6 +159,7 @@ 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) { @@ -255,25 +251,6 @@ trait ToTaffy { impl ToTaffy for Style { fn to_taffy(&self, rem_size: Pixels) -> taffy::style::Style { - use taffy::style_helpers::{fr, length, minmax, repeat}; - - fn to_grid_line( - placement: &Range, - ) -> taffy::Line { - taffy::Line { - start: placement.start.into(), - end: placement.end.into(), - } - } - - fn to_grid_repeat( - unit: &Option, - ) -> Vec> { - // grid-template-columns: repeat(, minmax(0, 1fr)); - unit.map(|count| vec![repeat(count, vec![minmax(length(0.0), fr(1.0))])]) - .unwrap_or_default() - } - taffy::style::Style { display: self.display.into(), overflow: self.overflow.into(), @@ -297,19 +274,7 @@ impl ToTaffy for Style { flex_basis: self.flex_basis.to_taffy(rem_size), flex_grow: self.flex_grow, flex_shrink: self.flex_shrink, - grid_template_rows: to_grid_repeat(&self.grid_rows), - grid_template_columns: to_grid_repeat(&self.grid_cols), - grid_row: self - .grid_location - .as_ref() - .map(|location| to_grid_line(&location.row)) - .unwrap_or_default(), - grid_column: self - .grid_location - .as_ref() - .map(|location| to_grid_line(&location.column)) - .unwrap_or_default(), - ..Default::default() + ..Default::default() // Ignore grid properties for now } } } diff --git a/crates/gpui/src/text_system.rs b/crates/gpui/src/text_system.rs index 53991089da..ed1307c6cd 100644 --- a/crates/gpui/src/text_system.rs +++ b/crates/gpui/src/text_system.rs @@ -65,7 +65,7 @@ impl TextSystem { font_runs_pool: Mutex::default(), fallback_font_stack: smallvec![ // TODO: Remove this when Linux have implemented setting fallbacks. - font(".ZedMono"), + font("Zed Plex Mono"), font("Helvetica"), font("Segoe UI"), // Windows font("Cantarell"), // Gnome @@ -96,7 +96,7 @@ impl TextSystem { } /// Get the FontId for the configure font family and style. - fn font_id(&self, font: &Font) -> Result { + pub fn font_id(&self, font: &Font) -> Result { fn clone_font_id_result(font_id: &Result) -> Result { match font_id { Ok(font_id) => Ok(*font_id), @@ -366,14 +366,15 @@ impl WindowTextSystem { let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new(); for run in runs { - 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; + 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; + } } decoration_runs.push(DecorationRun { len: run.len as u32, @@ -435,7 +436,7 @@ impl WindowTextSystem { }); } - if decoration_runs.last().is_some_and(|last_run| { + if decoration_runs.last().map_or(false, |last_run| { last_run.color == run.color && last_run.underline == run.underline && last_run.strikethrough == run.strikethrough @@ -491,14 +492,14 @@ impl WindowTextSystem { let mut split_lines = text.split('\n'); let mut processed = false; - 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()); + 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()); + } } } @@ -533,11 +534,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() - && last_run.font_id == font_id - { - last_run.len += run.len; - continue; + if let Some(last_run) = font_runs.last_mut() { + if last_run.font_id == font_id { + last_run.len += run.len; + continue; + } } font_runs.push(FontRun { len: run.len, @@ -843,16 +844,3 @@ impl FontMetrics { (self.bounding_box / self.units_per_em as f32 * font_size.0).map(px) } } - -#[allow(unused)] -pub(crate) fn font_name_with_fallbacks<'a>(name: &'a str, system: &'a str) -> &'a str { - // Note: the "Zed Plex" fonts were deprecated as we are not allowed to use "Plex" - // in a derived font name. They are essentially indistinguishable from IBM Plex/Lilex, - // and so retained here for backward compatibility. - match name { - ".SystemUIFont" => system, - ".ZedSans" | "Zed Plex Sans" => "IBM Plex Sans", - ".ZedMono" | "Zed Plex Mono" => "Lilex", - _ => name, - } -} diff --git a/crates/gpui/src/text_system/line.rs b/crates/gpui/src/text_system/line.rs index 8d559f9815..3813393d81 100644 --- a/crates/gpui/src/text_system/line.rs +++ b/crates/gpui/src/text_system/line.rs @@ -292,10 +292,10 @@ fn paint_line( } if let Some(style_run) = style_run { - if let Some((_, underline_style)) = &mut current_underline - && style_run.underline.as_ref() != Some(underline_style) - { - finished_underline = current_underline.take(); + 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(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 - && style_run.strikethrough.as_ref() != Some(strikethrough_style) - { - finished_strikethrough = current_strikethrough.take(); + 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(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 - && style_run.background_color.as_ref() != Some(background_color) - { - finished_background = current_background.take(); + 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(run_background) = style_run.background_color { current_background.get_or_insert(( diff --git a/crates/gpui/src/text_system/line_layout.rs b/crates/gpui/src/text_system/line_layout.rs index 43694702a8..9c2dd7f087 100644 --- a/crates/gpui/src/text_system/line_layout.rs +++ b/crates/gpui/src/text_system/line_layout.rs @@ -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 - && boundaries.len() >= max_lines - 1 - { - break; + if let Some(max_lines) = max_lines { + if boundaries.len() >= max_lines - 1 { + break; + } } if let Some(last_candidate_ix) = last_candidate_ix.take() { diff --git a/crates/gpui/src/text_system/line_wrapper.rs b/crates/gpui/src/text_system/line_wrapper.rs index 93ec6c854c..5de26511d3 100644 --- a/crates/gpui/src/text_system/line_wrapper.rs +++ b/crates/gpui/src/text_system/line_wrapper.rs @@ -44,7 +44,7 @@ impl LineWrapper { let mut prev_c = '\0'; let mut index = 0; let mut candidates = fragments - .iter() + .into_iter() .flat_map(move |fragment| fragment.wrap_boundary_candidates()) .peekable(); iter::from_fn(move || { @@ -327,7 +327,7 @@ mod tests { fn build_wrapper() -> LineWrapper { let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(0)); let cx = TestAppContext::build(dispatcher, None); - let id = cx.text_system().resolve_font(&font(".ZedMono")); + let id = cx.text_system().font_id(&font("Zed Plex Mono")).unwrap(); LineWrapper::new(id, px(16.), cx.text_system().platform_text_system.clone()) } diff --git a/crates/gpui/src/util.rs b/crates/gpui/src/util.rs index 3d7fa06e6c..5e92335fdc 100644 --- a/crates/gpui/src/util.rs +++ b/crates/gpui/src/util.rs @@ -1,11 +1,13 @@ -use crate::{BackgroundExecutor, Task}; -use std::{ - future::Future, - pin::Pin, - sync::atomic::{AtomicUsize, Ordering::SeqCst}, - task, - time::Duration, -}; +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; pub use util::*; @@ -58,63 +60,18 @@ pub trait FluentBuilder { where Self: Sized, { - 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 - where - Self: Sized; -} - -impl FutureExt for T { - fn with_timeout(self, timeout: Duration, executor: &BackgroundExecutor) -> WithTimeout - where - Self: Sized, - { - WithTimeout { - future: self, - timer: executor.timer(timeout), - } - } -} - -pub struct WithTimeout { - 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 Future for WithTimeout { - type Output = Result; - - fn poll(self: Pin<&mut Self>, cx: &mut task::Context) -> task::Poll { - // 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 { 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 - } + self.map(|this| { + if let Some(_) = option { + this + } else { + then(this) + } + }) } } #[cfg(any(test, feature = "test-support"))] -pub async fn smol_timeout(timeout: Duration, f: F) -> Result +pub async fn timeout(timeout: Duration, f: F) -> Result where F: Future, { @@ -123,7 +80,7 @@ where Err(()) }; let future = async move { Ok(f.await) }; - smol::future::FutureExt::race(timer, future).await + timer.race(future).await } /// Increment the given atomic counter if it is not zero. diff --git a/crates/gpui/src/view.rs b/crates/gpui/src/view.rs index 217971792e..f461e2f7d0 100644 --- a/crates/gpui/src/view.rs +++ b/crates/gpui/src/view.rs @@ -205,21 +205,22 @@ impl Element for AnyView { let content_mask = window.content_mask(); let text_style = window.text_style(); - 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; + 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); + return (None, element_state); + } } let refreshing = mem::replace(&mut window.refreshing, true); diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 0791dcc621..6ebb1cac40 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -79,13 +79,11 @@ pub enum DispatchPhase { impl DispatchPhase { /// Returns true if this represents the "bubble" phase. - #[inline] pub fn bubble(self) -> bool { self == DispatchPhase::Bubble } /// Returns true if this represents the "capture" phase. - #[inline] pub fn capture(self) -> bool { self == DispatchPhase::Capture } @@ -243,14 +241,14 @@ impl FocusId { pub fn contains_focused(&self, window: &Window, cx: &App) -> bool { window .focused(cx) - .is_some_and(|focused| self.contains(focused.id, window)) + .map_or(false, |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.is_some_and(|focused| focused.id.contains(*self, window)) + focused.map_or(false, |focused| focused.id.contains(*self, window)) } /// Obtains whether this handle contains the given handle in the most recently rendered frame. @@ -504,7 +502,7 @@ impl HitboxId { return true; } } - false + return false; } /// Checks if the hitbox with this ID contains the mouse and should handle scroll events. @@ -634,7 +632,7 @@ impl TooltipId { window .tooltip_bounds .as_ref() - .is_some_and(|tooltip_bounds| { + .map_or(false, |tooltip_bounds| { tooltip_bounds.id == *self && tooltip_bounds.bounds.contains(&window.mouse_position()) }) @@ -2453,7 +2451,7 @@ impl Window { /// time. pub fn get_asset(&mut self, source: &A::Source, cx: &mut App) -> Option { let (task, _) = cx.fetch_asset::(source); - task.now_or_never() + task.clone().now_or_never() } /// Obtain the current element offset. This method should only be called during the /// prepaint phase of element drawing. @@ -2814,7 +2812,7 @@ impl Window { content_mask: content_mask.scale(scale_factor), color: style.color.unwrap_or_default().opacity(element_opacity), thickness: style.thickness.scale(scale_factor), - wavy: if style.wavy { 1 } else { 0 }, + wavy: style.wavy, }); } @@ -2845,7 +2843,7 @@ impl Window { content_mask: content_mask.scale(scale_factor), thickness: style.thickness.scale(scale_factor), color: style.color.unwrap_or_default().opacity(opacity), - wavy: 0, + wavy: false, }); } @@ -3044,7 +3042,7 @@ impl Window { let tile = self .sprite_atlas - .get_or_insert_with(¶ms.into(), &mut || { + .get_or_insert_with(¶ms.clone().into(), &mut || { Ok(Some(( data.size(frame_index), Cow::Borrowed( @@ -3401,16 +3399,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() - && 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() { + 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) + } } true })); @@ -3444,12 +3442,12 @@ impl Window { 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; + 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; + } } false @@ -3688,8 +3686,7 @@ impl Window { ); if !match_result.to_replay.is_empty() { - self.replay_pending_input(match_result.to_replay, cx); - cx.propagate_event = true; + self.replay_pending_input(match_result.to_replay, cx) } if !match_result.pending.is_empty() { @@ -3731,7 +3728,7 @@ impl Window { self.dispatch_keystroke_observers( event, Some(binding.action), - match_result.context_stack, + match_result.context_stack.clone(), cx, ); self.pending_input_changed(cx); @@ -3864,11 +3861,11 @@ impl Window { if !cx.propagate_event { continue 'replay; } - 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) + 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) + } } } } @@ -4249,25 +4246,6 @@ impl Window { .on_action(action_type, Rc::new(listener)); } - /// Register an action listener on the window for the next frame if the condition is true. - /// The type of action is determined by the first parameter of the given listener. - /// When the next frame is rendered the listener will be cleared. - /// - /// This is a fairly low-level method, so prefer using action handlers on elements unless you have - /// a specific need to register a global listener. - pub fn on_action_when( - &mut self, - condition: bool, - action_type: TypeId, - listener: impl Fn(&dyn Any, DispatchPhase, &mut Window, &mut App) + 'static, - ) { - if condition { - self.next_frame - .dispatch_tree - .on_action(action_type, Rc::new(listener)); - } - } - /// Read information about the GPU backing this window. /// Currently returns None on Mac and Windows. pub fn gpu_specs(&self) -> Option { @@ -4309,15 +4287,15 @@ impl Window { cx: &mut App, f: impl FnOnce(&mut Option, &mut Self) -> R, ) -> R { - 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) - }); + 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) + }); + } } } f(&mut None, self) @@ -4389,13 +4367,15 @@ 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) - && let Some(hitbox) = self + { + if 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))); + } } } } @@ -4442,7 +4422,7 @@ impl Window { if let Some((_, inspector_id)) = self.hovered_inspector_hitbox(inspector, &self.rendered_frame) { - inspector.set_active_element_id(inspector_id, self); + inspector.set_active_element_id(inspector_id.clone(), self); } } }); @@ -4466,7 +4446,7 @@ impl Window { } } } - None + return None; } } @@ -4583,7 +4563,7 @@ impl WindowHandle { where C: AppContext, { - cx.read_window(self, |root_view, _cx| root_view) + cx.read_window(self, |root_view, _cx| root_view.clone()) } /// Check if this window is 'active'. @@ -4719,8 +4699,6 @@ pub enum ElementId { Path(Arc), /// A code location. CodeLocation(core::panic::Location<'static>), - /// A labeled child of an element. - NamedChild(Box, SharedString), } impl ElementId { @@ -4741,7 +4719,6 @@ impl Display for ElementId { ElementId::Uuid(uuid) => write!(f, "{}", uuid)?, ElementId::Path(path) => write!(f, "{}", path.display())?, ElementId::CodeLocation(location) => write!(f, "{}", location)?, - ElementId::NamedChild(id, name) => write!(f, "{}-{}", id, name)?, } Ok(()) @@ -4832,12 +4809,6 @@ impl From<(&'static str, u32)> for ElementId { } } -impl> From<(ElementId, T)> for ElementId { - fn from((id, name): (ElementId, T)) -> Self { - ElementId::NamedChild(Box::new(id), name.into()) - } -} - /// A rectangle to be rendered in the window at the given position and size. /// Passed as an argument [`Window::paint_quad`]. #[derive(Clone)] diff --git a/crates/gpui_macros/src/derive_inspector_reflection.rs b/crates/gpui_macros/src/derive_inspector_reflection.rs index 9c1cb503a8..fa22f95f9a 100644 --- a/crates/gpui_macros/src/derive_inspector_reflection.rs +++ b/crates/gpui_macros/src/derive_inspector_reflection.rs @@ -160,14 +160,16 @@ fn extract_doc_comment(attrs: &[Attribute]) -> Option { let mut doc_lines = Vec::new(); for attr in attrs { - 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()); + 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()); + } + } + } } } @@ -189,7 +191,7 @@ fn extract_cfg_attributes(attrs: &[Attribute]) -> Vec { 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").is_ok_and(|name| name == "gpui") + std::env::var("CARGO_PKG_NAME").map_or(false, |name| name == "gpui") } struct MacroExpander; diff --git a/crates/gpui_macros/src/gpui_macros.rs b/crates/gpui_macros/src/gpui_macros.rs index 0f1365be77..3a58af6705 100644 --- a/crates/gpui_macros/src/gpui_macros.rs +++ b/crates/gpui_macros/src/gpui_macros.rs @@ -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))]`. diff --git a/crates/gpui_macros/src/test.rs b/crates/gpui_macros/src/test.rs index 648d3499ed..2c52149897 100644 --- a/crates/gpui_macros/src/test.rs +++ b/crates/gpui_macros/src/test.rs @@ -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,27 @@ fn generate_test_function( } _ => {} } - } 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; + } 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.quit(); + dispatcher.run_until_parked(); + )); + inner_fn_args.extend(quote!(&mut #cx_varname,)); + continue; + } } } } @@ -215,48 +214,47 @@ fn generate_test_function( inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(_seed),)); continue; } - } 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!( + } 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!( drop(#cx_varname_lock); dispatcher.run_until_parked(); - #cx_varname.update(|cx| { cx.background_executor().forbid_parking(); cx.quit(); }); + #cx_varname.update(|cx| { cx.quit() }); dispatcher.run_until_parked(); )); - 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.quit(); + dispatcher.run_until_parked(); + )); + inner_fn_args.extend(quote!(&mut #cx_varname,)); + 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; - } - _ => {} } } } diff --git a/crates/gpui_macros/tests/derive_inspector_reflection.rs b/crates/gpui_macros/tests/derive_inspector_reflection.rs index a0adcb7801..522c0a62c4 100644 --- a/crates/gpui_macros/tests/derive_inspector_reflection.rs +++ b/crates/gpui_macros/tests/derive_inspector_reflection.rs @@ -34,6 +34,13 @@ 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)] @@ -63,6 +70,10 @@ impl Transform for Number { fn add_one(self) -> Self { Number(self.0 + 1) } + + fn cfg_included(self) -> Self { + Number(self.0) + } } #[test] @@ -72,13 +83,14 @@ fn test_derive_inspector_reflection() { // Get all methods that match the pattern fn(self) -> Self or fn(mut self) -> Self let methods = methods::(); - assert_eq!(methods.len(), 5); + assert_eq!(methods.len(), 6); 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); @@ -94,7 +106,9 @@ fn test_derive_inspector_reflection() { .invoke(num.clone()); assert_eq!(incremented, Number(6)); - let quadrupled = find_method::("quadruple").unwrap().invoke(num); + let quadrupled = find_method::("quadruple") + .unwrap() + .invoke(num.clone()); assert_eq!(quadrupled, Number(20)); // Try to invoke a non-existent method diff --git a/crates/gpui_tokio/Cargo.toml b/crates/gpui_tokio/Cargo.toml index 2d4abf4063..46d5eafd5a 100644 --- a/crates/gpui_tokio/Cargo.toml +++ b/crates/gpui_tokio/Cargo.toml @@ -13,7 +13,6 @@ 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"] } diff --git a/crates/gpui_tokio/src/gpui_tokio.rs b/crates/gpui_tokio/src/gpui_tokio.rs index 8384f2a88e..fffe18a616 100644 --- a/crates/gpui_tokio/src/gpui_tokio.rs +++ b/crates/gpui_tokio/src/gpui_tokio.rs @@ -52,28 +52,6 @@ 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(cx: &C, f: Fut) -> C::Result>> - where - C: AppContext, - Fut: Future> + 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() } diff --git a/crates/html_to_markdown/src/markdown.rs b/crates/html_to_markdown/src/markdown.rs index bb3b3563bc..b9ffbac79c 100644 --- a/crates/html_to_markdown/src/markdown.rs +++ b/crates/html_to_markdown/src/markdown.rs @@ -34,14 +34,15 @@ impl HandleTag for ParagraphHandler { tag: &HtmlElement, writer: &mut MarkdownWriter, ) -> StartTagOutcome { - 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.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.tag() == "p" { diff --git a/crates/http_client/Cargo.toml b/crates/http_client/Cargo.toml index f63bff295e..2045708ff2 100644 --- a/crates/http_client/Cargo.toml +++ b/crates/http_client/Cargo.toml @@ -23,8 +23,6 @@ futures.workspace = true http.workspace = true http-body.workspace = true log.workspace = true -parking_lot.workspace = true -reqwest.workspace = true serde.workspace = true serde_json.workspace = true url.workspace = true diff --git a/crates/http_client/src/async_body.rs b/crates/http_client/src/async_body.rs index 6b99a54a7d..88972d279c 100644 --- a/crates/http_client/src/async_body.rs +++ b/crates/http_client/src/async_body.rs @@ -40,7 +40,7 @@ impl AsyncBody { } pub fn from_bytes(bytes: Bytes) -> Self { - Self(Inner::Bytes(Cursor::new(bytes))) + Self(Inner::Bytes(Cursor::new(bytes.clone()))) } } @@ -88,17 +88,6 @@ impl From<&'static str> for AsyncBody { } } -impl TryFrom for AsyncBody { - type Error = anyhow::Error; - - fn try_from(value: reqwest::Body) -> Result { - value - .as_bytes() - .ok_or_else(|| anyhow::anyhow!("Underlying data is a stream")) - .map(|bytes| Self::from_bytes(Bytes::copy_from_slice(bytes))) - } -} - impl> From> for AsyncBody { fn from(body: Option) -> Self { match body { diff --git a/crates/http_client/src/github.rs b/crates/http_client/src/github.rs index 32efed8e72..a038915e2f 100644 --- a/crates/http_client/src/github.rs +++ b/crates/http_client/src/github.rs @@ -8,7 +8,6 @@ use url::Url; pub struct GitHubLspBinaryVersion { pub name: String, pub url: String, - pub digest: Option, } #[derive(Deserialize, Debug)] @@ -25,7 +24,6 @@ pub struct GithubRelease { pub struct GithubReleaseAsset { pub name: String, pub browser_download_url: String, - pub digest: Option, } pub async fn latest_github_release( @@ -71,19 +69,11 @@ pub async fn latest_github_release( } }; - let mut release = releases + releases .into_iter() .filter(|release| !require_assets || !release.assets.is_empty()) .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 - && let Some(stripped) = digest.strip_prefix("sha256:") - { - *digest = stripped.to_owned(); - } - }); - Ok(release) + .context("finding a prerelease") } pub async fn get_release_by_tag_name( diff --git a/crates/http_client/src/http_client.rs b/crates/http_client/src/http_client.rs index 62468573ed..06875718d9 100644 --- a/crates/http_client/src/http_client.rs +++ b/crates/http_client/src/http_client.rs @@ -7,15 +7,14 @@ use derive_more::Deref; use http::HeaderValue; pub use http::{self, Method, Request, Response, StatusCode, Uri}; -use futures::{ - FutureExt as _, - future::{self, BoxFuture}, -}; +use futures::future::BoxFuture; use http::request::Builder; -use parking_lot::Mutex; #[cfg(feature = "test-support")] use std::fmt; -use std::{any::type_name, sync::Arc}; +use std::{ + any::type_name, + sync::{Arc, Mutex}, +}; pub use url::Url; #[derive(Default, Debug, Clone, PartialEq, Eq, Hash)] @@ -87,19 +86,6 @@ pub trait HttpClient: 'static + Send + Sync { } fn proxy(&self) -> Option<&Url>; - - #[cfg(feature = "test-support")] - fn as_fake(&self) -> &FakeHttpClient { - panic!("called as_fake on {}", type_name::()) - } - - fn send_multipart_form<'a>( - &'a self, - _url: &str, - _request: reqwest::multipart::Form, - ) -> BoxFuture<'a, anyhow::Result>> { - future::ready(Err(anyhow!("not implemented"))).boxed() - } } /// An [`HttpClient`] that may have a proxy. @@ -146,18 +132,26 @@ impl HttpClient for HttpClientWithProxy { fn type_name(&self) -> &'static str { self.client.type_name() } +} - #[cfg(feature = "test-support")] - fn as_fake(&self) -> &FakeHttpClient { - self.client.as_fake() +impl HttpClient for Arc { + fn send( + &self, + req: Request, + ) -> BoxFuture<'static, anyhow::Result>> { + self.client.send(req) } - fn send_multipart_form<'a>( - &'a self, - url: &str, - form: reqwest::multipart::Form, - ) -> BoxFuture<'a, anyhow::Result>> { - self.client.send_multipart_form(url, form) + fn user_agent(&self) -> Option<&HeaderValue> { + self.client.user_agent() + } + + fn proxy(&self) -> Option<&Url> { + self.proxy.as_ref() + } + + fn type_name(&self) -> &'static str { + self.client.type_name() } } @@ -205,13 +199,20 @@ impl HttpClientWithUrl { /// Returns the base URL. pub fn base_url(&self) -> String { - self.base_url.lock().clone() + self.base_url + .lock() + .map_or_else(|_| Default::default(), |url| url.clone()) } /// Sets the base URL. pub fn set_base_url(&self, base_url: impl Into) { let base_url = base_url.into(); - *self.base_url.lock() = base_url; + self.base_url + .lock() + .map(|mut url| { + *url = base_url; + }) + .ok(); } /// Builds a URL using the given path. @@ -268,7 +269,7 @@ impl HttpClientWithUrl { } } -impl HttpClient for HttpClientWithUrl { +impl HttpClient for Arc { fn send( &self, req: Request, @@ -287,18 +288,26 @@ impl HttpClient for HttpClientWithUrl { fn type_name(&self) -> &'static str { self.client.type_name() } +} - #[cfg(feature = "test-support")] - fn as_fake(&self) -> &FakeHttpClient { - self.client.as_fake() +impl HttpClient for HttpClientWithUrl { + fn send( + &self, + req: Request, + ) -> BoxFuture<'static, anyhow::Result>> { + self.client.send(req) } - fn send_multipart_form<'a>( - &'a self, - url: &str, - request: reqwest::multipart::Form, - ) -> BoxFuture<'a, anyhow::Result>> { - self.client.send_multipart_form(url, request) + fn user_agent(&self) -> Option<&HeaderValue> { + self.client.user_agent() + } + + fn proxy(&self) -> Option<&Url> { + self.client.proxy.as_ref() + } + + fn type_name(&self) -> &'static str { + self.client.type_name() } } @@ -351,15 +360,10 @@ impl HttpClient for BlockedHttpClient { fn type_name(&self) -> &'static str { type_name::() } - - #[cfg(feature = "test-support")] - fn as_fake(&self) -> &FakeHttpClient { - panic!("called as_fake on {}", type_name::()) - } } #[cfg(feature = "test-support")] -type FakeHttpHandler = Arc< +type FakeHttpHandler = Box< dyn Fn(Request) -> BoxFuture<'static, anyhow::Result>> + Send + Sync @@ -368,7 +372,7 @@ type FakeHttpHandler = Arc< #[cfg(feature = "test-support")] pub struct FakeHttpClient { - handler: Mutex>, + handler: FakeHttpHandler, user_agent: HeaderValue, } @@ -383,7 +387,7 @@ impl FakeHttpClient { base_url: Mutex::new("http://test.example".into()), client: HttpClientWithProxy { client: Arc::new(Self { - handler: Mutex::new(Some(Arc::new(move |req| Box::pin(handler(req))))), + handler: Box::new(move |req| Box::pin(handler(req))), user_agent: HeaderValue::from_static(type_name::()), }), proxy: None, @@ -408,18 +412,6 @@ impl FakeHttpClient { .unwrap()) }) } - - pub fn replace_handler(&self, new_handler: F) - where - Fut: futures::Future>> + Send + 'static, - F: Fn(FakeHttpHandler, Request) -> Fut + Send + Sync + 'static, - { - let mut handler = self.handler.lock(); - let old_handler = handler.take().unwrap(); - *handler = Some(Arc::new(move |req| { - Box::pin(new_handler(old_handler.clone(), req)) - })); - } } #[cfg(feature = "test-support")] @@ -435,7 +427,8 @@ impl HttpClient for FakeHttpClient { &self, req: Request, ) -> BoxFuture<'static, anyhow::Result>> { - ((self.handler.lock().as_ref().unwrap())(req)) as _ + let future = (self.handler)(req); + future } fn user_agent(&self) -> Option<&HeaderValue> { @@ -449,8 +442,4 @@ impl HttpClient for FakeHttpClient { fn type_name(&self) -> &'static str { type_name::() } - - fn as_fake(&self) -> &FakeHttpClient { - self - } } diff --git a/crates/icons/README.md b/crates/icons/README.md deleted file mode 100644 index e340a00277..0000000000 --- a/crates/icons/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# Zed Icons - -## Guidelines - -Icons are a big part of Zed, and they're how we convey hundreds of actions without relying on labeled buttons. -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.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. -6. Avoid complex layer structures in the icon SVG, like clipping masks and similar elements. When the shape becomes too complex, we recommend running the SVG through [SVGOMG](https://jakearchibald.github.io/svgomg/) to clean it up. - -## Sourcing - -Most icons are created by sourcing them from [Lucide](https://lucide.dev/). -Then, they're modified, adjusted, cleaned up, and simplified depending on their use and overall fit with Zed. - -Sometimes, we may use other sources like [Phosphor](https://phosphoricons.com/), but we also design many icons completely from scratch. - -## Contributing - -To introduce a new icon, add the `.svg` file to the `assets/icon` directory and then add its corresponding item to the `icons.rs` file within the `crates` directory. - -- SVG files in the assets folder follow a snake_case name format. -- Icons in the `icons.rs` file follow the PascalCase name format. - -Make sure to tag a member of Zed's design team so we can review and adjust any newly introduced icon. diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index f7363395ae..7552060be4 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -28,13 +28,17 @@ pub enum IconName { ArrowCircle, ArrowDown, ArrowDown10, + ArrowDownFromLine, ArrowDownRight, ArrowLeft, ArrowRight, ArrowRightLeft, ArrowUp, + ArrowUpAlt, + ArrowUpFromLine, ArrowUpRight, - Attach, + ArrowUpRightAlt, + AtSign, AudioOff, AudioOn, Backspace, @@ -44,26 +48,34 @@ pub enum IconName { BellRing, Binary, Blocks, - BoltOutlined, + Bolt, BoltFilled, + BoltFilledAlt, Book, BookCopy, + BookPlus, + Brain, + BugOff, CaseSensitive, - Chat, Check, CheckDouble, ChevronDown, + /// This chevron indicates a popover menu. + ChevronDownSmall, ChevronLeft, ChevronRight, ChevronUp, ChevronUpDown, Circle, + CircleOff, CircleHelp, Close, + Cloud, CloudDownload, Code, Cog, Command, + Context, Control, Copilot, CopilotDisabled, @@ -84,67 +96,73 @@ pub enum IconName { DebugIgnoreBreakpoints, DebugLogBreakpoint, DebugPause, + DebugRestart, DebugStepBack, DebugStepInto, DebugStepOut, DebugStepOver, + DebugStop, + Delete, Diff, Disconnected, + DocumentText, Download, - EditorAtom, - EditorCursor, - EditorEmacs, - EditorJetBrains, - EditorSublime, - EditorVsCode, Ellipsis, EllipsisVertical, Envelope, + Equal, Eraser, Escape, Exit, ExpandDown, ExpandUp, ExpandVertical, + ExternalLink, Eye, File, FileCode, + FileCreate, FileDiff, FileDoc, FileGeneric, FileGit, FileLock, - FileMarkdown, FileRust, - FileTextFilled, - FileTextOutlined, + FileSearch, + FileText, FileToml, FileTree, Filter, Flame, Folder, FolderOpen, - FolderSearch, + FolderX, Font, FontSize, FontWeight, ForwardArrow, + Function, GenericClose, GenericMaximize, GenericMinimize, GenericRestore, GitBranch, - GitBranchAlt, + GitBranchSmall, Github, + Globe, + Hammer, Hash, HistoryRerun, Image, Indicator, Info, - Json, + InlayHint, Keyboard, + Layout, Library, + LightBulb, LineHeight, + Link, ListCollapse, ListTodo, ListTree, @@ -152,30 +170,42 @@ pub enum IconName { LoadCircle, LocationEdit, LockOutlined, + LspDebug, + LspRestart, + LspStop, MagnifyingGlass, + MailOpen, Maximize, Menu, MenuAlt, - MenuAltTemp, + MessageBubbles, Mic, MicMute, + Microscope, Minimize, - Notepad, + NewFromSummary, + NewTextThread, + NewThread, Option, PageDown, PageUp, + PanelLeft, + PanelRight, Pencil, - PencilUnavailable, Person, + PersonCircle, + PhoneIncoming, Pin, - PlayOutlined, + Play, + PlayAlt, + PlayBug, PlayFilled, Plus, + PocketKnife, Power, Public, PullRequest, Quote, - Reader, RefreshTitle, Regex, ReplNeutral, @@ -185,20 +215,31 @@ pub enum IconName { ReplyArrowRight, Rerun, Return, + Reveal, RotateCcw, RotateCw, + Route, + Save, Scissors, Screen, + ScrollText, + SearchSelection, SelectAll, Send, Server, Settings, - ShieldCheck, + SettingsAlt, Shift, Slash, + SlashSquare, Sliders, + SlidersVertical, + Snip, Space, Sparkle, + SparkleAlt, + SparkleFilled, + Spinner, Split, SplitAlt, SquareDot, @@ -207,6 +248,8 @@ pub enum IconName { Star, StarFilled, Stop, + StopFilled, + Strikethrough, Supermaven, SupermavenDisabled, SupermavenError, @@ -215,16 +258,13 @@ pub enum IconName { Tab, Terminal, TerminalAlt, - TerminalGhost, TextSnippet, - TextThread, - Thread, - ThreadFromSummary, ThumbsDown, ThumbsUp, TodoComplete, TodoPending, TodoProgress, + ToolBulb, ToolCopy, ToolDeleteFile, ToolDiagnostics, @@ -236,22 +276,25 @@ pub enum IconName { ToolRegex, ToolSearch, ToolTerminal, - ToolThink, ToolWeb, Trash, + TrashAlt, Triangle, TriangleRight, Undo, Unpin, + Update, UserCheck, UserGroup, UserRoundPen, + Visible, + Wand, Warning, WholeWord, + X, XCircle, - XCircleFilled, - ZedAgent, ZedAssistant, + ZedAssistantFilled, ZedBurnMode, ZedBurnModeOn, ZedMcpCustom, diff --git a/crates/image_viewer/src/image_viewer.rs b/crates/image_viewer/src/image_viewer.rs index 2dca57424b..b96557b391 100644 --- a/crates/image_viewer/src/image_viewer.rs +++ b/crates/image_viewer/src/image_viewer.rs @@ -401,19 +401,12 @@ pub fn init(cx: &mut App) { mod persistence { use std::path::PathBuf; - use db::{ - query, - sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection}, - sqlez_macros::sql, - }; + use db::{define_connection, query, sqlez_macros::sql}; use workspace::{ItemId, WorkspaceDb, WorkspaceId}; - pub struct ImageViewerDb(ThreadSafeConnection); - - impl Domain for ImageViewerDb { - const NAME: &str = stringify!(ImageViewerDb); - - const MIGRATIONS: &[&str] = &[sql!( + define_connection! { + pub static ref IMAGE_VIEWER: ImageViewerDb = + &[sql!( CREATE TABLE image_viewers ( workspace_id INTEGER, item_id INTEGER UNIQUE, @@ -424,11 +417,9 @@ 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( diff --git a/crates/indexed_docs/Cargo.toml b/crates/indexed_docs/Cargo.toml new file mode 100644 index 0000000000..eb269ad939 --- /dev/null +++ b/crates/indexed_docs/Cargo.toml @@ -0,0 +1,38 @@ +[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 diff --git a/crates/acp_tools/LICENSE-GPL b/crates/indexed_docs/LICENSE-GPL similarity index 100% rename from crates/acp_tools/LICENSE-GPL rename to crates/indexed_docs/LICENSE-GPL diff --git a/crates/indexed_docs/src/extension_indexed_docs_provider.rs b/crates/indexed_docs/src/extension_indexed_docs_provider.rs new file mode 100644 index 0000000000..c77ea4066d --- /dev/null +++ b/crates/indexed_docs/src/extension_indexed_docs_provider.rs @@ -0,0 +1,81 @@ +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, +} + +impl ExtensionIndexedDocsProviderProxy for IndexedDocsRegistryProxy { + fn register_indexed_docs_provider(&self, extension: Arc, provider_id: Arc) { + self.indexed_docs_registry + .register_provider(Box::new(ExtensionIndexedDocsProvider::new( + extension, + ProviderId(provider_id), + ))); + } + + fn unregister_indexed_docs_provider(&self, provider_id: Arc) { + self.indexed_docs_registry + .unregister_provider(&ProviderId(provider_id)); + } +} + +pub struct ExtensionIndexedDocsProvider { + extension: Arc, + id: ProviderId, +} + +impl ExtensionIndexedDocsProvider { + pub fn new(extension: Arc, 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> { + 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) -> Result<()> { + self.extension + .index_docs(self.id.0.clone(), package.as_ref().into(), database) + .await + } +} diff --git a/crates/indexed_docs/src/indexed_docs.rs b/crates/indexed_docs/src/indexed_docs.rs new file mode 100644 index 0000000000..97538329d4 --- /dev/null +++ b/crates/indexed_docs/src/indexed_docs.rs @@ -0,0 +1,16 @@ +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); +} diff --git a/crates/indexed_docs/src/providers.rs b/crates/indexed_docs/src/providers.rs new file mode 100644 index 0000000000..c6505a2ab6 --- /dev/null +++ b/crates/indexed_docs/src/providers.rs @@ -0,0 +1 @@ +pub mod rustdoc; diff --git a/crates/indexed_docs/src/providers/rustdoc.rs b/crates/indexed_docs/src/providers/rustdoc.rs new file mode 100644 index 0000000000..ac6dc3a10b --- /dev/null +++ b/crates/indexed_docs/src/providers/rustdoc.rs @@ -0,0 +1,291 @@ +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, +} + +pub struct LocalRustdocProvider { + fs: Arc, + cargo_workspace_root: PathBuf, +} + +impl LocalRustdocProvider { + pub fn id() -> ProviderId { + ProviderId("rustdoc".into()) + } + + pub fn new(fs: Arc, 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> { + static WORKSPACE_CRATES: LazyLock, 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::>(); + + *WORKSPACE_CRATES.write() = Some((workspace_crates.clone(), Instant::now())); + + Ok(workspace_crates.into_iter().collect()) + } + + async fn index(&self, package: PackageName, database: Arc) -> 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, +} + +impl DocsDotRsProvider { + pub fn id() -> ProviderId { + ProviderId("docs-rs".into()) + } + + pub fn new(http_client: Arc) -> 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> { + static POPULAR_CRATES: LazyLock> = 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) -> 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, + fetch_page: impl Fn( + &PackageName, + Option<&RustdocItem>, + ) -> BoxFuture<'static, Result>> + + 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 = + 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(()) +} diff --git a/crates/indexed_docs/src/providers/rustdoc/item.rs b/crates/indexed_docs/src/providers/rustdoc/item.rs new file mode 100644 index 0000000000..7d9023ef3e --- /dev/null +++ b/crates/indexed_docs/src/providers/rustdoc/item.rs @@ -0,0 +1,82 @@ +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>, + /// The name of the item. + pub name: Arc, +} + +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("/") + } +} diff --git a/crates/indexed_docs/src/providers/rustdoc/popular_crates.txt b/crates/indexed_docs/src/providers/rustdoc/popular_crates.txt new file mode 100644 index 0000000000..ce2c3d51d8 --- /dev/null +++ b/crates/indexed_docs/src/providers/rustdoc/popular_crates.txt @@ -0,0 +1,252 @@ +# 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 diff --git a/crates/indexed_docs/src/providers/rustdoc/to_markdown.rs b/crates/indexed_docs/src/providers/rustdoc/to_markdown.rs new file mode 100644 index 0000000000..87e3863728 --- /dev/null +++ b/crates/indexed_docs/src/providers/rustdoc/to_markdown.rs @@ -0,0 +1,618 @@ +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)> { + let item_collector = Rc::new(RefCell::new(RustdocItemCollector::new())); + + let mut handlers: Vec = 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::>(); + + 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, +} + +impl RustdocItemCollector { + pub fn new() -> Self { + Self { + items: IndexSet::new(), + } + } + + fn parse_item(tag: &HtmlElement) -> Option { + 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 { + 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##" + + "##}; + 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#" +

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.

+ "#}; + 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##" +

§Serde

+

Serde is a framework for serializing and deserializing 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.

+ "##}; + 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#" +

This text is bolded.

+

This text is italicized.

+ "#}; + 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#" +
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>) {}
+ "#}; + 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) {} + + // `Query` gives you the query parameters and deserializes them. + async fn query(Query(params): Query>) {} + + // 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) {} + ``` + "} + .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##" +

§Required dependencies

+

To use axum there are a few dependencies you have to pull in as well:

+
[dependencies]
+            axum = "<latest-version>"
+            tokio = { version = "<latest-version>", features = ["full"] }
+            tower = "<latest-version>"
+            
+ "##}; + let expected = indoc! {r#" + ## Required dependencies + + To use axum there are a few dependencies you have to pull in as well: + + ```toml + [dependencies] + axum = "" + tokio = { version = "", features = ["full"] } + tower = "" + + ``` + "#} + .trim(); + + assert_eq!( + convert_html_to_markdown(html.as_bytes(), &mut rustdoc_handlers()).unwrap(), + expected + ) + } + + #[test] + fn test_item_table() { + let html = indoc! {r##" +

Structs§

+
    +
  • Errors that can happen when using axum.
  • +
  • Extractor and response for extensions.
  • +
  • Formform
    URL encoded extractor and response.
  • +
  • Jsonjson
    JSON Extractor / Response.
  • +
  • The router type for composing handlers and services.
+

Functions§

+
    +
  • servetokio and (http1 or http2)
    Serve the service with the supplied listener.
  • +
+ "##}; + 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##" +

§Feature flags

+

axum uses a set of feature flags to reduce the amount of compiled and + optional dependencies.

+

The following optional features are available:

+
+ + + + + + + + + + + + + +
NameDescriptionDefault?
http1Enables hyper’s http1 featureYes
http2Enables hyper’s http2 featureNo
jsonEnables the Json type and some similar convenience functionalityYes
macrosEnables optional utility macrosNo
matched-pathEnables capturing of every request’s router path and the MatchedPath extractorYes
multipartEnables parsing multipart/form-data requests with MultipartNo
original-uriEnables capturing of every request’s original URI and the OriginalUri extractorYes
tokioEnables tokio as a dependency and axum::serve, SSE and extract::connect_info types.Yes
tower-logEnables tower’s log featureYes
tracingLog rejections from built-in extractorsYes
wsEnables WebSockets support via extract::wsNo
formEnables the Form extractorYes
queryEnables the Query extractorYes
+ "##}; + 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 + ) + } +} diff --git a/crates/indexed_docs/src/registry.rs b/crates/indexed_docs/src/registry.rs new file mode 100644 index 0000000000..6757cd9c1a --- /dev/null +++ b/crates/indexed_docs/src/registry.rs @@ -0,0 +1,62 @@ +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); + +impl Global for GlobalIndexedDocsRegistry {} + +pub struct IndexedDocsRegistry { + executor: BackgroundExecutor, + stores_by_provider: RwLock>>, +} + +impl IndexedDocsRegistry { + pub fn global(cx: &App) -> Arc { + 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 { + self.stores_by_provider + .read() + .keys() + .cloned() + .collect::>() + } + + pub fn register_provider( + &self, + provider: Box, + ) { + 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> { + self.stores_by_provider.read().get(&provider_id).cloned() + } +} diff --git a/crates/indexed_docs/src/store.rs b/crates/indexed_docs/src/store.rs new file mode 100644 index 0000000000..1407078efa --- /dev/null +++ b/crates/indexed_docs/src/store.rs @@ -0,0 +1,346 @@ +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); + +/// The name of a package. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Deref, Display)] +pub struct PackageName(Arc); + +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>; + + /// Indexes the package with the given name. + async fn index(&self, package: PackageName, database: Arc) -> Result<()>; +} + +/// A store for indexed docs. +pub struct IndexedDocsStore { + executor: BackgroundExecutor, + database_future: + Shared, Arc>>>, + provider: Box, + indexing_tasks_by_package: + RwLock>>>>>, + latest_errors_by_package: RwLock>>, +} + +impl IndexedDocsStore { + pub fn try_global(provider: ProviderId, cx: &App) -> Result> { + 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, + 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> { + 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 { + self.database_future + .clone() + .await + .map_err(|err| anyhow!(err))? + .load(key) + .await + } + + pub async fn load_many_by_prefix(&self, prefix: String) -> Result> { + 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 { + self.database_future + .clone() + .await + .map_err(|err| anyhow!(err))? + .any_with_prefix(prefix) + .await + } + + pub fn suggest_packages(self: Arc) -> Task>> { + let this = self.clone(); + self.executor + .spawn(async move { this.provider.suggest_packages().await }) + } + + pub fn index( + self: Arc, + package: PackageName, + ) -> Shared>>> { + 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> { + 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::>(); + + 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>, +} + +impl IndexedDocsDatabase { + pub fn new(path: PathBuf, executor: BackgroundExecutor) -> Result { + 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>> { + 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> { + 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>> { + 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::>(); + + Ok(results) + }) + } + + /// Returns whether any entries exist with the given prefix. + pub fn any_with_prefix(&self, prefix: String) -> Task> { + 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> { + 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> { + IndexedDocsDatabase::insert(&self, key, docs) + } +} diff --git a/crates/edit_prediction/Cargo.toml b/crates/inline_completion/Cargo.toml similarity index 82% rename from crates/edit_prediction/Cargo.toml rename to crates/inline_completion/Cargo.toml index 81c1e5dec2..3a90875def 100644 --- a/crates/edit_prediction/Cargo.toml +++ b/crates/inline_completion/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "edit_prediction" +name = "inline_completion" version = "0.1.0" edition.workspace = true publish.workspace = true @@ -9,7 +9,7 @@ license = "GPL-3.0-or-later" workspace = true [lib] -path = "src/edit_prediction.rs" +path = "src/inline_completion.rs" [dependencies] client.workspace = true diff --git a/crates/action_log/LICENSE-GPL b/crates/inline_completion/LICENSE-GPL similarity index 100% rename from crates/action_log/LICENSE-GPL rename to crates/inline_completion/LICENSE-GPL diff --git a/crates/edit_prediction/src/edit_prediction.rs b/crates/inline_completion/src/inline_completion.rs similarity index 90% rename from crates/edit_prediction/src/edit_prediction.rs rename to crates/inline_completion/src/inline_completion.rs index 6b695af1ae..c8f35bf16a 100644 --- a/crates/edit_prediction/src/edit_prediction.rs +++ b/crates/inline_completion/src/inline_completion.rs @@ -7,7 +7,7 @@ use project::Project; // TODO: Find a better home for `Direction`. // -// This should live in an ancestor crate of `editor` and `edit_prediction`, +// This should live in an ancestor crate of `editor` and `inline_completion`, // but at time of writing there isn't an obvious spot. #[derive(Copy, Clone, PartialEq, Eq)] pub enum Direction { @@ -16,7 +16,7 @@ pub enum Direction { } #[derive(Clone)] -pub struct EditPrediction { +pub struct InlineCompletion { /// The ID of the completion, if it has one. pub id: Option, pub edits: Vec<(Range, String)>, @@ -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 { @@ -61,10 +61,6 @@ pub trait EditPredictionProvider: 'static + Sized { fn show_tab_accept_marker() -> bool { false } - fn supports_jump_to_edit() -> bool { - true - } - fn data_collection_state(&self, _cx: &App) -> DataCollectionState { DataCollectionState::Unsupported } @@ -89,6 +85,9 @@ pub trait EditPredictionProvider: 'static + Sized { debounce: bool, cx: &mut Context, ); + fn needs_terms_acceptance(&self, _cx: &App) -> bool { + false + } fn cycle( &mut self, buffer: Entity, @@ -103,10 +102,10 @@ pub trait EditPredictionProvider: 'static + Sized { buffer: &Entity, cursor_position: language::Anchor, cx: &mut Context, - ) -> Option; + ) -> Option; } -pub trait EditPredictionProviderHandle { +pub trait InlineCompletionProviderHandle { fn name(&self) -> &'static str; fn display_name(&self) -> &'static str; fn is_enabled( @@ -117,10 +116,10 @@ pub trait EditPredictionProviderHandle { ) -> bool; fn show_completions_in_menu(&self) -> bool; fn show_tab_accept_marker(&self) -> bool; - fn supports_jump_to_edit(&self) -> bool; fn data_collection_state(&self, cx: &App) -> DataCollectionState; fn usage(&self, cx: &App) -> Option; 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, @@ -144,10 +143,10 @@ pub trait EditPredictionProviderHandle { buffer: &Entity, cursor_position: language::Anchor, cx: &mut App, - ) -> Option; + ) -> Option; } -impl EditPredictionProviderHandle for Entity +impl InlineCompletionProviderHandle for Entity where T: EditPredictionProvider, { @@ -167,10 +166,6 @@ where T::show_tab_accept_marker() } - fn supports_jump_to_edit(&self) -> bool { - T::supports_jump_to_edit() - } - fn data_collection_state(&self, cx: &App) -> DataCollectionState { self.read(cx).data_collection_state(cx) } @@ -192,6 +187,10 @@ 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() } @@ -234,7 +233,7 @@ where buffer: &Entity, cursor_position: language::Anchor, cx: &mut App, - ) -> Option { + ) -> Option { self.update(cx, |this, cx| this.suggest(buffer, cursor_position, cx)) } } diff --git a/crates/edit_prediction_button/Cargo.toml b/crates/inline_completion_button/Cargo.toml similarity index 89% rename from crates/edit_prediction_button/Cargo.toml rename to crates/inline_completion_button/Cargo.toml index 07447280fa..b34e59336b 100644 --- a/crates/edit_prediction_button/Cargo.toml +++ b/crates/inline_completion_button/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "edit_prediction_button" +name = "inline_completion_button" version = "0.1.0" edition.workspace = true publish.workspace = true @@ -9,7 +9,7 @@ license = "GPL-3.0-or-later" workspace = true [lib] -path = "src/edit_prediction_button.rs" +path = "src/inline_completion_button.rs" doctest = false [dependencies] @@ -22,10 +22,9 @@ feature_flags.workspace = true fs.workspace = true gpui.workspace = true indoc.workspace = true -edit_prediction.workspace = true +inline_completion.workspace = true language.workspace = true paths.workspace = true -project.workspace = true regex.workspace = true settings.workspace = true supermaven.workspace = true diff --git a/crates/agent2/LICENSE-GPL b/crates/inline_completion_button/LICENSE-GPL similarity index 100% rename from crates/agent2/LICENSE-GPL rename to crates/inline_completion_button/LICENSE-GPL diff --git a/crates/edit_prediction_button/src/edit_prediction_button.rs b/crates/inline_completion_button/src/inline_completion_button.rs similarity index 94% rename from crates/edit_prediction_button/src/edit_prediction_button.rs rename to crates/inline_completion_button/src/inline_completion_button.rs index 0e3fe8cb1a..d402b87382 100644 --- a/crates/edit_prediction_button/src/edit_prediction_button.rs +++ b/crates/inline_completion_button/src/inline_completion_button.rs @@ -1,8 +1,12 @@ use anyhow::Result; -use client::{UserStore, zed_urls}; +use client::{CloudUserStore, DisableAiSettings, zed_urls}; use cloud_llm_client::UsageLimit; use copilot::{Copilot, Status}; -use editor::{Editor, SelectionEffects, actions::ShowEditPrediction, scroll::Autoscroll}; +use editor::{ + Editor, SelectionEffects, + actions::{ShowEditPrediction, ToggleEditPrediction}, + scroll::Autoscroll, +}; use feature_flags::{FeatureFlagAppExt, PredictEditsRateCompletionsFeatureFlag}; use fs::Fs; use gpui::{ @@ -15,7 +19,6 @@ use language::{ EditPredictionsMode, File, Language, language_settings::{self, AllLanguageSettings, EditPredictionProvider, all_language_settings}, }; -use project::DisableAiSettings; use regex::Regex; use settings::{Settings, SettingsStore, update_settings_file}; use std::{ @@ -37,7 +40,7 @@ use zeta::RateCompletions; actions!( edit_prediction, [ - /// Toggles the edit prediction menu. + /// Toggles the inline completion menu. ToggleMenu ] ); @@ -47,16 +50,16 @@ const PRIVACY_DOCS: &str = "https://zed.dev/docs/ai/privacy-and-security"; struct CopilotErrorToast; -pub struct EditPredictionButton { +pub struct InlineCompletionButton { editor_subscription: Option<(Subscription, usize)>, editor_enabled: Option, editor_show_predictions: bool, editor_focus_handle: Option, language: Option>, file: Option>, - edit_prediction_provider: Option>, + edit_prediction_provider: Option>, fs: Arc, - user_store: Entity, + cloud_user_store: Entity, popover_menu_handle: PopoverMenuHandle, } @@ -67,7 +70,7 @@ enum SupermavenButtonStatus { Initializing, } -impl Render for EditPredictionButton { +impl Render for InlineCompletionButton { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { // Return empty div if AI is disabled if DisableAiSettings::get_global(cx).disable_ai { @@ -127,7 +130,7 @@ impl Render for EditPredictionButton { }), ); } - let this = cx.entity(); + let this = cx.entity().clone(); div().child( PopoverMenu::new("copilot") @@ -168,7 +171,7 @@ impl Render for EditPredictionButton { let account_status = agent.account_status.clone(); match account_status { AccountStatus::NeedsActivation { activate_url } => { - SupermavenButtonStatus::NeedsActivation(activate_url) + SupermavenButtonStatus::NeedsActivation(activate_url.clone()) } AccountStatus::Unknown => SupermavenButtonStatus::Initializing, AccountStatus::Ready => SupermavenButtonStatus::Ready, @@ -182,10 +185,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(); + let this = cx.entity().clone(); let fs = self.fs.clone(); - div().child( + return div().child( PopoverMenu::new("supermaven") .menu(move |window, cx| match &status { SupermavenButtonStatus::NeedsActivation(activate_url) => { @@ -230,7 +233,7 @@ impl Render for EditPredictionButton { }, ) .with_handle(self.popover_menu_handle.clone()), - ) + ); } EditPredictionProvider::Zed => { @@ -242,9 +245,13 @@ impl Render for EditPredictionButton { IconName::ZedPredictDisabled }; - if zeta::should_show_upsell_modal() { - let tooltip_meta = if self.user_store.read(cx).current_user().is_some() { - "Choose a Plan" + if zeta::should_show_upsell_modal(&self.cloud_user_store, cx) { + let tooltip_meta = if self.cloud_user_store.read(cx).is_authenticated() { + if self.cloud_user_store.read(cx).has_accepted_tos() { + "Choose a Plan" + } else { + "Accept the Terms of Service" + } } else { "Sign In" }; @@ -327,7 +334,7 @@ impl Render for EditPredictionButton { }) }); - let this = cx.entity(); + let this = cx.entity().clone(); let mut popover_menu = PopoverMenu::new("zeta") .menu(move |window, cx| { @@ -339,7 +346,7 @@ impl Render for EditPredictionButton { let is_refreshing = self .edit_prediction_provider .as_ref() - .is_some_and(|provider| provider.is_refreshing(cx)); + .map_or(false, |provider| provider.is_refreshing(cx)); if is_refreshing { popover_menu = popover_menu.trigger( @@ -361,10 +368,10 @@ impl Render for EditPredictionButton { } } -impl EditPredictionButton { +impl InlineCompletionButton { pub fn new( fs: Arc, - user_store: Entity, + cloud_user_store: Entity, popover_menu_handle: PopoverMenuHandle, cx: &mut Context, ) -> Self { @@ -383,9 +390,9 @@ impl EditPredictionButton { language: None, file: None, edit_prediction_provider: None, - user_store, popover_menu_handle, fs, + cloud_user_store, } } @@ -433,13 +440,9 @@ impl EditPredictionButton { if let Some(editor_focus_handle) = self.editor_focus_handle.clone() { let entry = ContextMenuEntry::new("This Buffer") .toggleable(IconPosition::Start, self.editor_show_predictions) - .action(Box::new(editor::actions::ToggleEditPrediction)) + .action(Box::new(ToggleEditPrediction)) .handler(move |window, cx| { - editor_focus_handle.dispatch_action( - &editor::actions::ToggleEditPrediction, - window, - cx, - ); + editor_focus_handle.dispatch_action(&ToggleEditPrediction, window, cx); }); match language_state.clone() { @@ -466,7 +469,7 @@ impl EditPredictionButton { IconPosition::Start, None, move |_, cx| { - toggle_show_edit_predictions_for_language(language.clone(), fs.clone(), cx) + toggle_show_inline_completions_for_language(language.clone(), fs.clone(), cx) }, ); } @@ -474,25 +477,17 @@ impl EditPredictionButton { let settings = AllLanguageSettings::get_global(cx); let globally_enabled = settings.show_edit_predictions(None, cx); - let entry = ContextMenuEntry::new("All Files") - .toggleable(IconPosition::Start, globally_enabled) - .action(workspace::ToggleEditPrediction.boxed_clone()) - .handler(|window, cx| { - window.dispatch_action(workspace::ToggleEditPrediction.boxed_clone(), cx) - }); - menu = menu.item(entry); + menu = menu.toggleable_entry("All Files", globally_enabled, IconPosition::Start, None, { + let fs = fs.clone(); + move |_, cx| toggle_inline_completions_globally(fs.clone(), cx) + }); let provider = settings.edit_predictions.provider; let current_mode = settings.edit_predictions_mode(); let subtle_mode = matches!(current_mode, EditPredictionsMode::Subtle); let eager_mode = matches!(current_mode, EditPredictionsMode::Eager); - if matches!( - provider, - EditPredictionProvider::Zed - | EditPredictionProvider::Copilot - | EditPredictionProvider::Supermaven - ) { + if matches!(provider, EditPredictionProvider::Zed) { menu = menu .separator() .header("Display Modes") @@ -768,7 +763,7 @@ impl EditPredictionButton { }) }) .separator(); - } else if self.user_store.read(cx).account_too_young() { + } else if self.cloud_user_store.read(cx).account_too_young() { menu = menu .custom_entry( |_window, _cx| { @@ -783,7 +778,7 @@ impl EditPredictionButton { cx.open_url(&zed_urls::account_url(cx)) }) .separator(); - } else if self.user_store.read(cx).has_overdue_invoices() { + } else if self.cloud_user_store.read(cx).has_overdue_invoices() { menu = menu .custom_entry( |_window, _cx| { @@ -839,7 +834,7 @@ impl EditPredictionButton { } } -impl StatusItemView for EditPredictionButton { +impl StatusItemView for InlineCompletionButton { fn set_active_pane_item( &mut self, item: Option<&dyn ItemHandle>, @@ -909,7 +904,7 @@ async fn open_disabled_globs_setting_in_editor( let settings = cx.global::(); - // Ensure that we always have "edit_predictions { "disabled_globs": [] }" + // Ensure that we always have "inline_completions { "disabled_globs": [] }" let edits = settings.edits_for_update::(&text, |file| { file.edit_predictions .get_or_insert_with(Default::default) @@ -947,6 +942,13 @@ async fn open_disabled_globs_setting_in_editor( anyhow::Ok(()) } +fn toggle_inline_completions_globally(fs: Arc, cx: &mut App) { + let show_edit_predictions = all_language_settings(None, cx).show_edit_predictions(None, cx); + update_settings_file::(fs, cx, move |file, _| { + file.defaults.show_edit_predictions = Some(!show_edit_predictions) + }); +} + fn set_completion_provider(fs: Arc, cx: &mut App, provider: EditPredictionProvider) { update_settings_file::(fs, cx, move |file, _| { file.features @@ -955,7 +957,7 @@ fn set_completion_provider(fs: Arc, cx: &mut App, provider: EditPredicti }); } -fn toggle_show_edit_predictions_for_language( +fn toggle_show_inline_completions_for_language( language: Arc, fs: Arc, cx: &mut App, diff --git a/crates/inspector_ui/Cargo.toml b/crates/inspector_ui/Cargo.toml index cefe888974..8e55a8a477 100644 --- a/crates/inspector_ui/Cargo.toml +++ b/crates/inspector_ui/Cargo.toml @@ -24,7 +24,6 @@ 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 diff --git a/crates/inspector_ui/src/div_inspector.rs b/crates/inspector_ui/src/div_inspector.rs index c3d687e57a..bd395aa01b 100644 --- a/crates/inspector_ui/src/div_inspector.rs +++ b/crates/inspector_ui/src/div_inspector.rs @@ -25,7 +25,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 = util_macros::path!("/zed-inspector-style.json"); +const ZED_INSPECTOR_STYLE_JSON: &str = "/zed-inspector-style.json"; pub(crate) struct DivInspector { state: State, @@ -93,8 +93,8 @@ impl DivInspector { Ok((json_style_buffer, rust_style_buffer)) => { this.update_in(cx, |this, window, cx| { this.state = State::BuffersLoaded { - json_style_buffer, - rust_style_buffer, + json_style_buffer: json_style_buffer, + rust_style_buffer: rust_style_buffer, }; // Initialize editors immediately instead of waiting for @@ -200,8 +200,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| { - if event == &EditorEvent::BufferEdited { + move |this, editor, event: &EditorEvent, window, cx| match event { + EditorEvent::BufferEdited => { let style_json = editor.read(cx).text(cx); match serde_json_lenient::from_str_lenient::(&style_json) { Ok(new_style) => { @@ -243,6 +243,7 @@ impl DivInspector { Err(err) => this.json_style_error = Some(err.to_string().into()), } } + _ => {} } }) .detach(); @@ -250,10 +251,11 @@ 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| { - if let EditorEvent::BufferEdited = event { + move |this, _editor, event: &EditorEvent, cx| match event { + EditorEvent::BufferEdited => { this.update_json_style_from_rust(&json_style_buffer, &rust_style_buffer, cx); } + _ => {} } }) .detach(); @@ -269,19 +271,23 @@ impl DivInspector { } fn reset_style(&mut self, cx: &mut App) { - 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; + 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; + } } + _ => {} } } @@ -389,11 +395,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::(); 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::(); @@ -696,10 +702,10 @@ impl CompletionProvider for RustStyleCompletionProvider { } fn completion_replace_range(snapshot: &BufferSnapshot, anchor: &Anchor) -> Option> { - 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()?; diff --git a/crates/install_cli/src/install_cli.rs b/crates/install_cli/src/install_cli.rs index dc9e0e31ab..12c094448b 100644 --- a/crates/install_cli/src/install_cli.rs +++ b/crates/install_cli/src/install_cli.rs @@ -105,7 +105,7 @@ pub fn install_cli(window: &mut Window, cx: &mut Context) { cx, ) })?; - register_zed_scheme(cx).await.log_err(); + register_zed_scheme(&cx).await.log_err(); Ok(()) }) .detach_and_prompt_err("Error installing zed cli", window, cx, |_, _, _| None); diff --git a/crates/jj/src/jj_repository.rs b/crates/jj/src/jj_repository.rs index afbe54c99d..93ae79eb90 100644 --- a/crates/jj/src/jj_repository.rs +++ b/crates/jj/src/jj_repository.rs @@ -50,13 +50,16 @@ impl RealJujutsuRepository { impl JujutsuRepository for RealJujutsuRepository { fn list_bookmarks(&self) -> Vec { - self.repository + let bookmarks = self + .repository .view() .bookmarks() .map(|(ref_name, _target)| Bookmark { ref_name: ref_name.as_str().to_string().into(), }) - .collect() + .collect(); + + bookmarks } } diff --git a/crates/jj/src/jj_store.rs b/crates/jj/src/jj_store.rs index 2d2d958d7f..a10f06fad4 100644 --- a/crates/jj/src/jj_store.rs +++ b/crates/jj/src/jj_store.rs @@ -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; }; diff --git a/crates/journal/src/journal.rs b/crates/journal/src/journal.rs index c09ab6f764..0335a746cd 100644 --- a/crates/journal/src/journal.rs +++ b/crates/journal/src/journal.rs @@ -123,7 +123,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(); + let view_snapshot = workspace.weak_handle().clone(); window .spawn(cx, async move |cx| { @@ -170,23 +170,23 @@ pub fn new_journal_entry(workspace: &Workspace, window: &mut Window, cx: &mut Ap .await }; - if let Some(Some(Ok(item))) = opened.first() - && let Some(editor) = item.downcast::().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 { + if let Some(Some(Ok(item))) = opened.first() { + if let Some(editor) = item.downcast::().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); - } - editor.insert(&entry_heading, window, cx); - editor.insert("\n\n", window, cx); - })?; + })?; + } } anyhow::Ok(()) @@ -195,9 +195,11 @@ pub fn new_journal_entry(workspace: &Workspace, window: &mut Window, cx: &mut Ap } fn journal_dir(path: &str) -> Option { - shellexpand::full(path) //TODO handle this better + let expanded_journal_dir = shellexpand::full(path) //TODO handle this better .ok() - .map(|dir| Path::new(&dir.to_string()).to_path_buf().join("journal")) + .map(|dir| Path::new(&dir.to_string()).to_path_buf().join("journal")); + + expanded_journal_dir } fn heading_entry(now: NaiveTime, hour_format: &Option) -> String { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 4ddc2b3018..83517accc2 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -716,7 +716,7 @@ impl EditPreview { &self.applied_edits_snapshot, &self.syntax_snapshot, None, - syntax_theme, + &syntax_theme, ); } @@ -727,7 +727,7 @@ impl EditPreview { ¤t_snapshot.text, ¤t_snapshot.syntax, Some(deletion_highlight_style), - syntax_theme, + &syntax_theme, ); } @@ -737,7 +737,7 @@ impl EditPreview { &self.applied_edits_snapshot, &self.syntax_snapshot, Some(insertion_highlight_style), - syntax_theme, + &syntax_theme, ); } @@ -749,7 +749,7 @@ impl EditPreview { &self.applied_edits_snapshot, &self.syntax_snapshot, None, - syntax_theme, + &syntax_theme, ); highlighted_text.build() @@ -974,6 +974,8 @@ 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); } @@ -1018,6 +1020,9 @@ 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 { @@ -1123,7 +1128,7 @@ impl Buffer { } else { ranges.as_slice() } - .iter() + .into_iter() .peekable(); let mut edits = Vec::new(); @@ -1153,12 +1158,13 @@ impl Buffer { base_buffer.edit(edits, None, cx) }); - if let Some(operation) = operation - && let Some(BufferBranchState { + if let Some(operation) = operation { + if let Some(BufferBranchState { merged_operations, .. }) = &mut self.branch_state - { - merged_operations.push(operation); + { + merged_operations.push(operation); + } } } @@ -1179,11 +1185,11 @@ impl Buffer { }; let mut operation_to_undo = None; - 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); + 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); + } } self.apply_ops([operation.clone()], cx); @@ -1390,8 +1396,7 @@ impl Buffer { is_first = false; return true; } - - layer + let any_sub_ranges_contain_range = layer .included_sub_ranges .map(|sub_ranges| { sub_ranges.iter().any(|sub_range| { @@ -1400,7 +1405,9 @@ impl Buffer { !is_before_start && !is_after_end }) }) - .unwrap_or(true) + .unwrap_or(true); + let result = any_sub_ranges_contain_range; + return result; }) .last() .map(|info| info.language.clone()) @@ -1417,10 +1424,10 @@ impl Buffer { .map(|info| info.language.clone()) .collect(); - if languages.is_empty() - && let Some(buffer_language) = self.language() - { - languages.push(buffer_language.clone()); + if languages.is_empty() { + if let Some(buffer_language) = self.language() { + languages.push(buffer_language.clone()); + } } languages @@ -1514,12 +1521,12 @@ impl Buffer { let new_syntax_map = parse_task.await; this.update(cx, move |this, cx| { let grammar_changed = - this.language.as_ref().is_none_or(|current_language| { + this.language.as_ref().map_or(true, |current_language| { !Arc::ptr_eq(&language, current_language) }); let language_registry_changed = new_syntax_map .contains_unknown_injections() - && language_registry.is_some_and(|registry| { + && language_registry.map_or(false, |registry| { registry.version() != new_syntax_map.language_registry_version() }); let parse_again = language_registry_changed @@ -1564,26 +1571,15 @@ 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 buffer_diagnostics( - &self, - for_server: Option, - ) -> Vec<&DiagnosticEntry> { - 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(), - } + 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) } fn request_autoindent(&mut self, cx: &mut Context) { @@ -1723,7 +1719,8 @@ impl Buffer { }) .with_delta(suggestion.delta, language_indent_size); - if old_suggestions.get(&new_row).is_none_or( + if old_suggestions.get(&new_row).map_or( + true, |(old_indentation, was_within_error)| { suggested_indent != *old_indentation && (!suggestion.within_error || *was_within_error) @@ -2017,7 +2014,7 @@ impl Buffer { fn was_changed(&mut self) { self.change_bits.retain(|change_bit| { - change_bit.upgrade().is_some_and(|bit| { + change_bit.upgrade().map_or(false, |bit| { bit.replace(true); true }) @@ -2194,7 +2191,7 @@ impl Buffer { if self .remote_selections .get(&self.text.replica_id()) - .is_none_or(|set| !set.selections.is_empty()) + .map_or(true, |set| !set.selections.is_empty()) { self.set_active_selections(Arc::default(), false, Default::default(), cx); } @@ -2211,7 +2208,7 @@ impl Buffer { self.remote_selections.insert( AGENT_REPLICA_ID, SelectionSet { - selections, + selections: selections.clone(), lamport_timestamp, line_mode, cursor_shape, @@ -2273,11 +2270,13 @@ 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() - && prev_range.end >= range.start - { - prev_range.end = cmp::max(prev_range.end, range.end); - *prev_text = format!("{prev_text}{new_text}").into(); + 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)); + } } else { edits.push((range, new_text)); } @@ -2297,27 +2296,10 @@ impl Buffer { if let Some((before_edit, mode)) = autoindent_request { let mut delta = 0isize; - let mut previous_setting = None; - let entries: Vec<_> = edits + let entries = 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); @@ -2391,14 +2373,12 @@ impl Buffer { }) .collect(); - 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.autoindent_requests.push(Arc::new(AutoindentRequest { + before_edit, + entries, + is_block_mode: matches!(mode, AutoindentMode::Block { .. }), + ignore_empty_lines: false, + })); } self.end_transaction(cx); @@ -2591,10 +2571,10 @@ impl Buffer { line_mode, cursor_shape, } => { - if let Some(set) = self.remote_selections.get(&lamport_timestamp.replica_id) - && set.lamport_timestamp > lamport_timestamp - { - return; + if let Some(set) = self.remote_selections.get(&lamport_timestamp.replica_id) { + if set.lamport_timestamp > lamport_timestamp { + return; + } } self.remote_selections.insert( @@ -2620,7 +2600,7 @@ impl Buffer { self.completion_triggers = self .completion_triggers_per_language_server .values() - .flat_map(|triggers| triggers.iter().cloned()) + .flat_map(|triggers| triggers.into_iter().cloned()) .collect(); } else { self.completion_triggers_per_language_server @@ -2780,7 +2760,7 @@ impl Buffer { self.completion_triggers = self .completion_triggers_per_language_server .values() - .flat_map(|triggers| triggers.iter().cloned()) + .flat_map(|triggers| triggers.into_iter().cloned()) .collect(); } else { self.completion_triggers_per_language_server @@ -2842,7 +2822,7 @@ impl Buffer { let mut edits: Vec<(Range, String)> = Vec::new(); let mut last_end = None; for _ in 0..old_range_count { - if last_end.is_some_and(|last_end| last_end >= self.len()) { + if last_end.map_or(false, |last_end| last_end >= self.len()) { break; } @@ -3011,9 +2991,9 @@ impl BufferSnapshot { } let mut error_ranges = Vec::>::new(); - let mut matches = self - .syntax - .matches(range, &self.text, |grammar| grammar.error_query.as_ref()); + let mut matches = self.syntax.matches(range.clone(), &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()); @@ -3062,14 +3042,14 @@ impl BufferSnapshot { if config .decrease_indent_pattern .as_ref() - .is_some_and(|regex| regex.is_match(line)) + .map_or(false, |regex| regex.is_match(line)) { indent_change_rows.push((row, Ordering::Less)); } if config .increase_indent_pattern .as_ref() - .is_some_and(|regex| regex.is_match(line)) + .map_or(false, |regex| regex.is_match(line)) { indent_change_rows.push((row + 1, Ordering::Greater)); } @@ -3085,7 +3065,7 @@ impl BufferSnapshot { } } for rule in &config.decrease_indent_patterns { - if rule.pattern.as_ref().is_some_and(|r| r.is_match(line)) { + if rule.pattern.as_ref().map_or(false, |r| r.is_match(line)) { let row_start_column = self.indent_size_for_line(row).len; let basis_row = rule .valid_after @@ -3298,7 +3278,8 @@ impl BufferSnapshot { range: Range, ) -> Option> { let range = range.to_offset(self); - self.syntax + return self + .syntax .layers_for_range(range, &self.text, false) .max_by(|a, b| { if a.depth != b.depth { @@ -3308,7 +3289,7 @@ impl BufferSnapshot { } else { a.node().end_byte().cmp(&b.node().end_byte()).reverse() } - }) + }); } /// Returns the main [`Language`]. @@ -3366,8 +3347,9 @@ impl BufferSnapshot { } } - if let Some(range) = range - && smallest_range_and_depth.as_ref().is_none_or( + if let Some(range) = range { + if smallest_range_and_depth.as_ref().map_or( + true, |(smallest_range, smallest_range_depth)| { if layer.depth > *smallest_range_depth { true @@ -3377,13 +3359,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), + }); + } } } @@ -3499,17 +3481,17 @@ 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 - && (right_node.is_named() || !left_node.is_named()) - { - layer_result = right_node; + if let Some(right_node) = right_node { + if 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; + if let Some(previous_result) = &result { + if previous_result.byte_range().len() < layer_result.byte_range().len() { + continue; + } } result = Some(layer_result); } @@ -3544,7 +3526,7 @@ impl BufferSnapshot { } } - Some(cursor.node()) + return Some(cursor.node()); } /// Returns the outline for the buffer. @@ -3573,7 +3555,7 @@ impl BufferSnapshot { )?; let mut prev_depth = None; items.retain(|item| { - let result = prev_depth.is_none_or(|prev_depth| item.depth > prev_depth); + let result = prev_depth.map_or(true, |prev_depth| item.depth > prev_depth); prev_depth = Some(item.depth); result }); @@ -4080,11 +4062,11 @@ impl BufferSnapshot { // Get the ranges of the innermost pair of brackets. let mut result: Option<(Range, Range)> = None; - 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; + 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; + } } let len = pair.close_range.end - pair.open_range.start; @@ -4253,7 +4235,7 @@ impl BufferSnapshot { .map(|(range, name)| { ( name.to_string(), - self.text_for_range(range).collect::(), + self.text_for_range(range.clone()).collect::(), ) }) .collect(); @@ -4450,7 +4432,7 @@ impl BufferSnapshot { pub fn words_in_range(&self, query: WordsQuery) -> BTreeMap> { let query_str = query.fuzzy_contents; - if query_str.is_some_and(|query| query.is_empty()) { + if query_str.map_or(false, |query| query.is_empty()) { return BTreeMap::default(); } @@ -4474,26 +4456,27 @@ impl BufferSnapshot { current_word_start_ix = Some(ix); } - if let Some(query_chars) = &query_chars - && query_ix < query_len - && c.to_lowercase().eq(query_chars[query_ix].to_lowercase()) - { - query_ix += 1; + 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; + } + } } continue; - } 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); + } 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); + } } } query_ix = 0; @@ -4606,17 +4589,17 @@ impl<'a> BufferChunks<'a> { highlights .stack .retain(|(end_offset, _)| *end_offset > range.start); - 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), - )); + 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(); } - highlights.next_capture.take(); } } else if let Some(snapshot) = self.buffer_snapshot { let (captures, highlight_maps) = snapshot.get_highlights(self.range.clone()); @@ -4641,33 +4624,33 @@ impl<'a> BufferChunks<'a> { } fn initialize_diagnostic_endpoints(&mut self) { - if let Some(diagnostics) = self.diagnostic_endpoints.as_mut() - && let Some(buffer) = self.buffer_snapshot - { - let mut diagnostic_endpoints = Vec::new(); - for entry in buffer.diagnostics_in_range::<_, usize>(self.range.clone(), false) { - diagnostic_endpoints.push(DiagnosticEndpoint { - offset: entry.range.start, - is_start: true, - severity: entry.diagnostic.severity, - is_unnecessary: entry.diagnostic.is_unnecessary, - underline: entry.diagnostic.underline, - }); - diagnostic_endpoints.push(DiagnosticEndpoint { - offset: entry.range.end, - is_start: false, - severity: entry.diagnostic.severity, - is_unnecessary: entry.diagnostic.is_unnecessary, - underline: entry.diagnostic.underline, - }); + if let Some(diagnostics) = self.diagnostic_endpoints.as_mut() { + if let Some(buffer) = self.buffer_snapshot { + let mut diagnostic_endpoints = Vec::new(); + for entry in buffer.diagnostics_in_range::<_, usize>(self.range.clone(), false) { + diagnostic_endpoints.push(DiagnosticEndpoint { + offset: entry.range.start, + is_start: true, + severity: entry.diagnostic.severity, + is_unnecessary: entry.diagnostic.is_unnecessary, + underline: entry.diagnostic.underline, + }); + diagnostic_endpoints.push(DiagnosticEndpoint { + offset: entry.range.end, + is_start: false, + severity: entry.diagnostic.severity, + is_unnecessary: entry.diagnostic.is_unnecessary, + underline: entry.diagnostic.underline, + }); + } + diagnostic_endpoints + .sort_unstable_by_key(|endpoint| (endpoint.offset, !endpoint.is_start)); + *diagnostics = diagnostic_endpoints.into_iter().peekable(); + self.hint_depth = 0; + self.error_depth = 0; + self.warning_depth = 0; + self.information_depth = 0; } - diagnostic_endpoints - .sort_unstable_by_key(|endpoint| (endpoint.offset, !endpoint.is_start)); - *diagnostics = diagnostic_endpoints.into_iter().peekable(); - self.hint_depth = 0; - self.error_depth = 0; - self.warning_depth = 0; - self.information_depth = 0; } } @@ -4778,11 +4761,11 @@ impl<'a> Iterator for BufferChunks<'a> { .min(next_capture_start) .min(next_diagnostic_endpoint); let mut highlight_id = None; - if let Some(highlights) = self.highlights.as_ref() - && let Some((parent_capture_end, parent_highlight_id)) = highlights.stack.last() - { - chunk_end = chunk_end.min(*parent_capture_end); - highlight_id = Some(*parent_highlight_id); + if let Some(highlights) = self.highlights.as_ref() { + if let Some((parent_capture_end, parent_highlight_id)) = highlights.stack.last() { + chunk_end = chunk_end.min(*parent_capture_end); + highlight_id = Some(*parent_highlight_id); + } } let slice = @@ -4976,12 +4959,11 @@ pub(crate) fn contiguous_ranges( std::iter::from_fn(move || { loop { if let Some(value) = values.next() { - if let Some(range) = &mut current_range - && value == range.end - && range.len() < max_len - { - range.end += 1; - continue; + if let Some(range) = &mut current_range { + if value == range.end && range.len() < max_len { + range.end += 1; + continue; + } } let prev_range = current_range.clone(); @@ -5049,10 +5031,10 @@ impl CharClassifier { } else { scope.word_characters() }; - if let Some(characters) = characters - && characters.contains(&c) - { - return CharKind::Word; + if let Some(characters) = characters { + if characters.contains(&c) { + return CharKind::Word; + } } } diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index ce65afa628..2e2df7e658 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -1744,7 +1744,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: original_indent_columns.clone(), }), cx, ); @@ -1790,9 +1790,9 @@ fn test_autoindent_block_mode_with_newline(cx: &mut App) { "# .unindent(); buffer.edit( - [(Point::new(2, 0)..Point::new(2, 0), inserted_text)], + [(Point::new(2, 0)..Point::new(2, 0), inserted_text.clone())], Some(AutoindentMode::Block { - original_indent_columns, + original_indent_columns: original_indent_columns.clone(), }), cx, ); @@ -1843,7 +1843,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: original_indent_columns.clone(), }), cx, ); @@ -2030,7 +2030,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); + language_registry.add(javascript_language.clone()); cx.new(|cx| { let (text, ranges) = marked_text_ranges( diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 7ae77c9141..549afc931c 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -44,7 +44,6 @@ 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, @@ -60,6 +59,7 @@ 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,9 +67,7 @@ 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, LocalLanguageToolchainStore, Toolchain, ToolchainList, ToolchainLister, -}; +pub use toolchain::{LanguageToolchainStore, Toolchain, ToolchainList, ToolchainLister}; use tree_sitter::{self, Query, QueryCursor, WasmStore, wasmtime}; use util::serde::default_true; @@ -121,8 +119,8 @@ where func(cursor.deref_mut()) } -static NEXT_LANGUAGE_ID: AtomicUsize = AtomicUsize::new(0); -static NEXT_GRAMMAR_ID: AtomicUsize = AtomicUsize::new(0); +static NEXT_LANGUAGE_ID: LazyLock = LazyLock::new(Default::default); +static NEXT_GRAMMAR_ID: LazyLock = LazyLock::new(Default::default); static WASM_ENGINE: LazyLock = LazyLock::new(|| { wasmtime::Engine::new(&wasmtime::Config::new()).expect("Failed to create Wasmtime engine") }); @@ -163,10 +161,11 @@ pub struct CachedLspAdapter { pub name: LanguageServerName, pub disk_based_diagnostic_sources: Vec, pub disk_based_diagnostics_progress_token: Option, - language_ids: HashMap, + language_ids: HashMap, pub adapter: Arc, pub reinstall_attempt_count: AtomicU64, cached_binary: futures::lock::Mutex>, + manifest_name: OnceLock>, } impl Debug for CachedLspAdapter { @@ -202,17 +201,18 @@ 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() + self.adapter.name().clone() } pub async fn get_language_server_command( self: Arc, delegate: Arc, - toolchains: Option, + toolchains: Arc, binary_options: LanguageServerBinaryOptions, cx: &mut AsyncApp, ) -> Result { @@ -277,10 +277,24 @@ impl CachedLspAdapter { pub fn language_id(&self, language_name: &LanguageName) -> String { self.language_ids - .get(language_name) + .get(language_name.as_ref()) .cloned() .unwrap_or_else(|| language_name.lsp_id()) } + pub fn manifest_name(&self) -> Option { + 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 @@ -312,7 +326,7 @@ pub trait LspAdapter: 'static + Send + Sync { fn get_language_server_command<'a>( self: Arc, delegate: Arc, - toolchains: Option, + toolchains: Arc, binary_options: LanguageServerBinaryOptions, mut cached_binary: futures::lock::MutexGuard<'a, Option>, cx: &'a mut AsyncApp, @@ -329,9 +343,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 - && let Some(binary) = self.check_if_user_installed(delegate.as_ref(), toolchains, cx).await { - log::debug!( + if binary_options.allow_path_lookup { + if let Some(binary) = self.check_if_user_installed(delegate.as_ref(), toolchains, cx).await { + log::info!( "found user-installed language server for {}. path: {:?}, arguments: {:?}", self.name().0, binary.path, @@ -339,6 +353,7 @@ pub trait LspAdapter: 'static + Send + Sync { ); return Ok(binary); } + } anyhow::ensure!(binary_options.allow_binary_download, "downloading language servers disabled"); @@ -386,7 +401,7 @@ pub trait LspAdapter: 'static + Send + Sync { async fn check_if_user_installed( &self, _: &dyn LspAdapterDelegate, - _: Option, + _: Arc, _: &AsyncApp, ) -> Option { None @@ -519,7 +534,7 @@ pub trait LspAdapter: 'static + Send + Sync { self: Arc, _: &dyn Fs, _: &Arc, - _: Option, + _: Arc, _cx: &mut AsyncApp, ) -> Result { Ok(serde_json::json!({})) @@ -539,6 +554,7 @@ pub trait LspAdapter: 'static + Send + Sync { _target_language_server_id: LanguageServerName, _: &dyn Fs, _: &Arc, + _: Arc, _cx: &mut AsyncApp, ) -> Result> { Ok(None) @@ -557,8 +573,8 @@ pub trait LspAdapter: 'static + Send + Sync { None } - fn language_ids(&self) -> HashMap { - HashMap::default() + fn language_ids(&self) -> HashMap { + Default::default() } /// Support custom initialize params. @@ -570,6 +586,17 @@ 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 { + 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 @@ -601,7 +628,7 @@ async fn try_fetch_server_binary } let name = adapter.name(); - log::debug!("fetching latest version of language server {:?}", name.0); + log::info!("fetching latest version of language server {:?}", name.0); delegate.update_status(name.clone(), BinaryStatus::CheckingForUpdate); let latest_version = adapter @@ -612,7 +639,7 @@ async fn try_fetch_server_binary .check_if_version_installed(latest_version.as_ref(), &container_dir, delegate.as_ref()) .await { - log::debug!("language server {:?} is already installed", name.0); + log::info!("language server {:?} is already installed", name.0); delegate.update_status(name.clone(), BinaryStatus::None); Ok(binary) } else { @@ -963,11 +990,11 @@ where fn deserialize_regex_vec<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { let sources = Vec::::deserialize(d)?; - sources - .into_iter() - .map(|source| regex::Regex::new(&source)) - .collect::>() - .map_err(de::Error::custom) + let mut regexes = Vec::new(); + for source in sources { + regexes.push(regex::Regex::new(&source).map_err(de::Error::custom)?); + } + Ok(regexes) } fn regex_vec_json_schema(_: &mut SchemaGenerator) -> schemars::Schema { @@ -1033,10 +1060,12 @@ impl<'de> Deserialize<'de> for BracketPairConfig { D: Deserializer<'de>, { let result = Vec::::deserialize(deserializer)?; - let (brackets, disabled_scopes_by_bracket_ix) = result - .into_iter() - .map(|entry| (entry.bracket_pair, entry.not_in)) - .unzip(); + 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); + } Ok(BracketPairConfig { pairs: brackets, @@ -1078,7 +1107,6 @@ pub struct Language { pub(crate) grammar: Option>, pub(crate) context_provider: Option>, pub(crate) toolchain: Option>, - pub(crate) manifest_name: Option, } #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] @@ -1289,7 +1317,6 @@ impl Language { }), context_provider: None, toolchain: None, - manifest_name: None, } } @@ -1303,10 +1330,6 @@ impl Language { self } - pub fn with_manifest(mut self, name: Option) -> Self { - self.manifest_name = name; - self - } pub fn with_queries(mut self, queries: LanguageQueries) -> Result { if let Some(query) = queries.highlights { self = self @@ -1376,14 +1399,16 @@ impl Language { let grammar = self.grammar_mut().context("cannot mutate grammar")?; let query = Query::new(&grammar.ts_language, source)?; - let extra_captures: Vec<_> = query - .capture_names() - .iter() - .map(|&name| match name { - "run" => RunnableCapture::Run, - name => RunnableCapture::Named(name.to_string().into()), - }) - .collect(); + 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); + } grammar.runnable_config = Some(RunnableConfig { extra_captures, @@ -1513,8 +1538,9 @@ impl Language { .map(|ix| { let mut config = BracketsPatternConfig::default(); for setting in query.property_settings(ix) { - if setting.key.as_ref() == "newline.only" { - config.newline_only = true + match setting.key.as_ref() { + "newline.only" => config.newline_only = true, + _ => {} } } config @@ -1737,9 +1763,6 @@ 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 { self.config @@ -1774,10 +1797,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 - && !highlight_id.is_default() - { - result.push((offset..end_offset, highlight_id)); + if let Some(highlight_id) = chunk.syntax_highlight_id { + if !highlight_id.is_default() { + result.push((offset..end_offset, highlight_id)); + } } offset = end_offset; } @@ -1794,11 +1817,11 @@ impl Language { } pub fn set_theme(&self, theme: &SyntaxTheme) { - 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); + 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); + } } } @@ -1828,7 +1851,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 { @@ -1918,11 +1941,11 @@ impl LanguageScope { .enumerate() .map(move |(ix, bracket)| { let mut is_enabled = true; - if let Some(next_disabled_ix) = disabled_ids.first() - && ix == *next_disabled_ix as usize - { - disabled_ids = &disabled_ids[1..]; - is_enabled = false; + if let Some(next_disabled_ix) = disabled_ids.first() { + if ix == *next_disabled_ix as usize { + disabled_ids = &disabled_ids[1..]; + is_enabled = false; + } } (bracket, is_enabled) }) @@ -2185,7 +2208,7 @@ impl LspAdapter for FakeLspAdapter { async fn check_if_user_installed( &self, _: &dyn LspAdapterDelegate, - _: Option, + _: Arc, _: &AsyncApp, ) -> Option { Some(self.language_server_binary.clone()) @@ -2194,7 +2217,7 @@ impl LspAdapter for FakeLspAdapter { fn get_language_server_command<'a>( self: Arc, _: Arc, - _: Option, + _: Arc, _: LanguageServerBinaryOptions, _: futures::lock::MutexGuard<'a, Option>, _: &'a mut AsyncApp, @@ -2329,9 +2352,9 @@ mod tests { assert_eq!( languages.language_names(), &[ - LanguageName::new("JSON"), - LanguageName::new("Plain Text"), - LanguageName::new("Rust"), + "JSON".to_string(), + "Plain Text".to_string(), + "Rust".to_string(), ] ); @@ -2342,9 +2365,9 @@ mod tests { assert_eq!( languages.language_names(), &[ - LanguageName::new("JSON"), - LanguageName::new("Plain Text"), - LanguageName::new("Rust"), + "JSON".to_string(), + "Plain Text".to_string(), + "Rust".to_string(), ] ); @@ -2355,9 +2378,9 @@ mod tests { assert_eq!( languages.language_names(), &[ - LanguageName::new("JSON"), - LanguageName::new("Plain Text"), - LanguageName::new("Rust"), + "JSON".to_string(), + "Plain Text".to_string(), + "Rust".to_string(), ] ); diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index 4f07240e44..ab3c0f9b37 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -1,6 +1,6 @@ use crate::{ CachedLspAdapter, File, Language, LanguageConfig, LanguageId, LanguageMatcher, - LanguageServerName, LspAdapter, ManifestName, PLAIN_TEXT, ToolchainLister, + LanguageServerName, LspAdapter, 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,7 +172,6 @@ pub struct AvailableLanguage { hidden: bool, load: Arc Result + 'static + Send + Sync>, loaded: bool, - manifest_name: Option, } impl AvailableLanguage { @@ -260,7 +259,6 @@ pub struct LoadedLanguage { pub queries: LanguageQueries, pub context_provider: Option>, pub toolchain_provider: Option>, - pub manifest_name: Option, } impl LanguageRegistry { @@ -351,14 +349,12 @@ 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, }) }), ) @@ -415,6 +411,30 @@ impl LanguageRegistry { cached } + pub fn get_or_register_lsp_adapter( + &self, + language_name: LanguageName, + server_name: LanguageServerName, + build_adapter: impl FnOnce() -> Arc + 'static, + ) -> Arc { + let registered = self + .state + .write() + .lsp_adapters + .entry(language_name.clone()) + .or_default() + .iter() + .find(|cached_adapter| cached_adapter.name == server_name) + .cloned(); + + if let Some(found) = registered { + found + } else { + let adapter = build_adapter(); + self.register_lsp_adapter(language_name, adapter) + } + } + /// Register a fake language server and adapter /// The returned channel receives a new instance of the language server every time it is started #[cfg(any(feature = "test-support", test))] @@ -432,7 +452,7 @@ impl LanguageRegistry { let mut state = self.state.write(); state .lsp_adapters - .entry(language_name) + .entry(language_name.clone()) .or_default() .push(adapter.clone()); state.all_lsp_adapters.insert(adapter.name(), adapter); @@ -454,7 +474,7 @@ impl LanguageRegistry { let cached_adapter = CachedLspAdapter::new(Arc::new(adapter)); state .lsp_adapters - .entry(language_name) + .entry(language_name.clone()) .or_default() .push(cached_adapter.clone()); state @@ -491,7 +511,6 @@ impl LanguageRegistry { grammar_name: Option>, matcher: LanguageMatcher, hidden: bool, - manifest_name: Option, load: Arc Result + 'static + Send + Sync>, ) { let state = &mut *self.state.write(); @@ -501,7 +520,6 @@ impl LanguageRegistry { existing_language.grammar = grammar_name; existing_language.matcher = matcher; existing_language.load = load; - existing_language.manifest_name = manifest_name; return; } } @@ -514,7 +532,6 @@ impl LanguageRegistry { load, hidden, loaded: false, - manifest_name, }); state.version += 1; state.reload_count += 1; @@ -554,15 +571,15 @@ impl LanguageRegistry { self.state.read().language_settings.clone() } - pub fn language_names(&self) -> Vec { + pub fn language_names(&self) -> Vec { let state = self.state.read(); let mut result = state .available_languages .iter() - .filter_map(|l| l.loaded.not().then_some(l.name.clone())) - .chain(state.languages.iter().map(|l| l.config.name.clone())) + .filter_map(|l| l.loaded.not().then_some(l.name.to_string())) + .chain(state.languages.iter().map(|l| l.config.name.to_string())) .collect::>(); - result.sort_unstable_by_key(|language_name| language_name.as_ref().to_lowercase()); + result.sort_unstable_by_key(|language_name| language_name.to_lowercase()); result } @@ -582,7 +599,6 @@ 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, }); @@ -773,7 +789,7 @@ impl LanguageRegistry { }; let content_matches = || { - config.first_line_pattern.as_ref().is_some_and(|pattern| { + config.first_line_pattern.as_ref().map_or(false, |pattern| { content .as_ref() .is_some_and(|content| pattern.is_match(content)) @@ -922,12 +938,10 @@ 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)) } } @@ -1102,7 +1116,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, @@ -1167,7 +1181,8 @@ 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; diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 0f82d3997f..9b0abb1537 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -5,7 +5,7 @@ use anyhow::Result; use collections::{FxHashMap, HashMap, HashSet}; use ec4rs::{ Properties as EditorconfigProperties, - property::{FinalNewline, IndentSize, IndentStyle, MaxLineLen, TabWidth, TrimTrailingWs}, + property::{FinalNewline, IndentSize, IndentStyle, TabWidth, TrimTrailingWs}, }; use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder}; use gpui::{App, Modifiers}; @@ -133,8 +133,6 @@ 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. @@ -187,8 +185,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::>(); @@ -199,7 +197,7 @@ impl LanguageSettings { if language_server.0.as_ref() == Self::REST_OF_LANGUAGE_SERVERS { rest.clone() } else { - vec![language_server] + vec![language_server.clone()] } }) .collect::>() @@ -253,7 +251,7 @@ impl EditPredictionSettings { !self.disabled_globs.iter().any(|glob| { if glob.is_absolute { file.as_local() - .is_some_and(|local| glob.matcher.is_match(local.abs_path(cx))) + .map_or(false, |local| glob.matcher.is_match(local.abs_path(cx))) } else { glob.matcher.is_match(file.path()) } @@ -350,12 +348,6 @@ 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 @@ -365,7 +357,7 @@ pub struct CompletionSettings { /// When set to 0, waits indefinitely. /// /// Default: 0 - #[serde(default)] + #[serde(default = "default_lsp_fetch_timeout_ms")] pub lsp_fetch_timeout_ms: u64, /// Controls how LSP completions are inserted. /// @@ -411,8 +403,8 @@ fn default_lsp_insert_mode() -> LspInsertMode { LspInsertMode::ReplaceSuffix } -fn default_3() -> usize { - 3 +fn default_lsp_fetch_timeout_ms() -> u64 { + 0 } /// The settings for a particular language. @@ -569,10 +561,6 @@ pub struct LanguageSettingsContent { /// /// Default: true pub linked_edits: Option, - /// Whether indentation should be adjusted based on the context whilst typing. - /// - /// Default: true - pub auto_indent: Option, /// Whether indentation of pasted content should be adjusted based on the context. /// /// Default: true @@ -999,7 +987,7 @@ pub struct InlayHintSettings { /// Default: false #[serde(default)] pub enabled: bool, - /// Global switch to toggle inline values on and off when debugging. + /// Global switch to toggle inline values on and off. /// /// Default: true #[serde(default = "default_true")] @@ -1137,10 +1125,6 @@ impl AllLanguageSettings { } fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigProperties) { - let preferred_line_length = cfg.get::().ok().and_then(|v| match v { - MaxLineLen::Value(u) => Some(u as u32), - MaxLineLen::Off => None, - }); let tab_size = cfg.get::().ok().and_then(|v| match v { IndentSize::Value(u) => NonZeroU32::new(u as u32), IndentSize::UseTabWidth => cfg.get::().ok().and_then(|w| match w { @@ -1168,7 +1152,6 @@ 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( @@ -1474,7 +1457,6 @@ 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, @@ -1535,7 +1517,6 @@ 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, @@ -1805,7 +1786,7 @@ mod tests { assert!(!settings.enabled_for_file(&dot_env_file, &cx)); // Test tilde expansion - let home = shellexpand::tilde("~").into_owned(); + let home = shellexpand::tilde("~").into_owned().to_string(); let home_file = make_test_file(&[&home, "test.rs"]); let settings = build_settings(&["~/test.rs"]); assert!(!settings.enabled_for_file(&home_file, &cx)); diff --git a/crates/language/src/manifest.rs b/crates/language/src/manifest.rs index 3ca0ddf71d..37505fec3b 100644 --- a/crates/language/src/manifest.rs +++ b/crates/language/src/manifest.rs @@ -12,12 +12,6 @@ impl Borrow for ManifestName { } } -impl Borrow for ManifestName { - fn borrow(&self) -> &str { - &self.0 - } -} - impl From for ManifestName { fn from(value: SharedString) -> Self { Self(value) diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index 3be189cea0..18f6bb8709 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -86,7 +86,7 @@ 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.clone(), + triggers: triggers.iter().cloned().collect(), language_server_id: server_id.to_proto(), }, ), @@ -385,10 +385,12 @@ pub fn deserialize_undo_map_entry( /// Deserializes selections from the RPC representation. pub fn deserialize_selections(selections: Vec) -> Arc<[Selection]> { - selections - .into_iter() - .filter_map(deserialize_selection) - .collect() + Arc::from( + selections + .into_iter() + .filter_map(deserialize_selection) + .collect::>(), + ) } /// Deserializes a [`Selection`] from the RPC representation. diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index 38aad007fe..f441114a90 100644 --- a/crates/language/src/syntax_map.rs +++ b/crates/language/src/syntax_map.rs @@ -17,7 +17,7 @@ use std::{ sync::Arc, }; use streaming_iterator::StreamingIterator; -use sum_tree::{Bias, Dimensions, SeekTarget, SumTree}; +use sum_tree::{Bias, SeekTarget, SumTree}; use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point, Rope, ToOffset, ToPoint}; use tree_sitter::{Node, Query, QueryCapture, QueryCaptures, QueryCursor, QueryMatches, Tree}; @@ -285,7 +285,7 @@ impl SyntaxSnapshot { pub fn interpolate(&mut self, text: &BufferSnapshot) { let edits = text - .anchored_edits_since::>(&self.interpolated_version) + .anchored_edits_since::<(usize, Point)>(&self.interpolated_version) .collect::>(); self.interpolated_version = text.version().clone(); @@ -333,8 +333,7 @@ impl SyntaxSnapshot { }; let Some(layer) = cursor.item() else { break }; - let Dimensions(start_byte, start_point, _) = - layer.range.start.summary::>(text); + let (start_byte, start_point) = layer.range.start.summary::<(usize, Point)>(text); // Ignore edits that end before the start of this layer, and don't consider them // for any subsequent layers at this same depth. @@ -414,42 +413,42 @@ impl SyntaxSnapshot { .collect::>(); self.reparse_with_ranges(text, root_language.clone(), edit_ranges, registry.as_ref()); - 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); - } - + 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(); - } - drop(cursor); + 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); + } - if !resolved_injection_ranges.is_empty() { - self.reparse_with_ranges( - text, - root_language, - resolved_injection_ranges, - Some(®istry), - ); + 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.language_registry_version = registry.version(); } self.update_count += 1; @@ -563,8 +562,8 @@ impl SyntaxSnapshot { } let Some(step) = step else { break }; - let Dimensions(step_start_byte, step_start_point, _) = - step.range.start.summary::>(text); + let (step_start_byte, step_start_point) = + step.range.start.summary::<(usize, Point)>(text); let step_end_byte = step.range.end.to_offset(text); let mut old_layer = cursor.item(); @@ -832,7 +831,7 @@ impl SyntaxSnapshot { query: fn(&Grammar) -> Option<&Query>, ) -> SyntaxMapCaptures<'a> { SyntaxMapCaptures::new( - range, + range.clone(), text, [SyntaxLayer { language, @@ -1065,10 +1064,10 @@ impl<'a> SyntaxMapCaptures<'a> { pub fn set_byte_range(&mut self, range: Range) { for layer in &mut self.layers { layer.captures.set_byte_range(range.clone()); - if let Some(capture) = &layer.next_capture - && capture.node.end_byte() > range.start - { - continue; + if let Some(capture) = &layer.next_capture { + if capture.node.end_byte() > range.start { + continue; + } } layer.advance(); } @@ -1277,11 +1276,11 @@ fn join_ranges( (None, None) => break, }; - if let Some(last) = result.last_mut() - && range.start <= last.end - { - last.end = last.end.max(range.end); - continue; + if let Some(last) = result.last_mut() { + if range.start <= last.end { + last.end = last.end.max(range.end); + continue; + } } result.push(range); } @@ -1297,7 +1296,7 @@ fn parse_text( ) -> anyhow::Result { 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,13 +1329,14 @@ 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) - && let Some(language) = language_registry + if let (Some(language_name), true) = (pattern.language.as_ref(), pattern.combined) { + if 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())); + } } } @@ -1356,11 +1356,10 @@ 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 - && mat.pattern_index == *prev_pattern_ix - && content_range == *prev_range - { - continue; + if let Some((prev_pattern_ix, prev_range)) = &prev_match { + if mat.pattern_index == *prev_pattern_ix && content_range == *prev_range { + continue; + } } prev_match = Some((mat.pattern_index, content_range.clone())); @@ -1630,8 +1629,10 @@ 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 { diff --git a/crates/language/src/syntax_map/syntax_map_tests.rs b/crates/language/src/syntax_map/syntax_map_tests.rs index 622731b781..d576c95cd5 100644 --- a/crates/language/src/syntax_map/syntax_map_tests.rs +++ b/crates/language/src/syntax_map/syntax_map_tests.rs @@ -58,7 +58,8 @@ fn test_splice_included_ranges() { assert_eq!(change, 0..1); // does not create overlapping ranges - let (new_ranges, change) = splice_included_ranges(ranges, &[0..18], &[ts_range(20..32)]); + let (new_ranges, change) = + splice_included_ranges(ranges.clone(), &[0..18], &[ts_range(20..32)]); assert_eq!( new_ranges, &[ts_range(20..32), ts_range(50..60), ts_range(80..90)] @@ -103,7 +104,7 @@ fn test_syntax_map_layers_for_range(cx: &mut App) { ); let mut syntax_map = SyntaxMap::new(&buffer); - syntax_map.set_language_registry(registry); + syntax_map.set_language_registry(registry.clone()); syntax_map.reparse(language.clone(), &buffer); assert_layers_for_range( @@ -164,7 +165,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, &buffer); + syntax_map.reparse(language.clone(), &buffer); assert_layers_for_range( &syntax_map, @@ -251,8 +252,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, &buffer); - syntax_map.reparse(markdown_inline, &buffer); + syntax_map.reparse(markdown.clone(), &buffer); + syntax_map.reparse(markdown_inline.clone(), &buffer); assert_layers_for_range( &syntax_map, &buffer, @@ -861,7 +862,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, &buffer); + syntax_map.reparse(language.clone(), &buffer); assert_capture_ranges( &syntax_map, @@ -985,7 +986,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); + reference_syntax_map.set_language_registry(registry.clone()); log::info!("initial text:\n{}", buffer.text()); diff --git a/crates/language/src/text_diff.rs b/crates/language/src/text_diff.rs index 11d8a070d2..f9221f571a 100644 --- a/crates/language/src/text_diff.rs +++ b/crates/language/src/text_diff.rs @@ -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], + &old_text[old_byte_range.clone()], options.language_scope.clone(), )); hunk_input.update_after(tokenize( - &new_text[new_byte_range], + &new_text[new_byte_range.clone()], 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].into() + new_text[new_byte_range.clone()].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].into() + new_text[new_byte_range.clone()].into() }; - edits.push((old_byte_range, replacement_text)); + edits.push((old_byte_range.clone(), replacement_text)); } }, ); @@ -154,19 +154,19 @@ fn diff_internal( input, |old_tokens: Range, new_tokens: Range| { 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) -> impl Iterator< let mut prev = None; let mut start_ix = 0; iter::from_fn(move || { - for (ix, c) in chars.by_ref() { + while let Some((ix, c)) = chars.next() { let mut token = None; let kind = classifier.kind(c); - 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; + 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; + } } prev = Some((c, kind)); if token.is_some() { diff --git a/crates/language/src/toolchain.rs b/crates/language/src/toolchain.rs index 73c142c8ca..1f4b038f68 100644 --- a/crates/language/src/toolchain.rs +++ b/crates/language/src/toolchain.rs @@ -17,7 +17,7 @@ use settings::WorktreeId; use crate::{LanguageName, ManifestName}; /// Represents a single toolchain. -#[derive(Clone, Debug, Eq)] +#[derive(Clone, Debug)] pub struct Toolchain { /// User-facing label pub name: SharedString, @@ -27,14 +27,6 @@ pub struct Toolchain { pub as_json: serde_json::Value, } -impl std::hash::Hash for Toolchain { - fn hash(&self, state: &mut H) { - self.name.hash(state); - self.path.hash(state); - self.language_name.hash(state); - } -} - impl PartialEq for Toolchain { fn eq(&self, other: &Self) -> bool { // Do not use as_json for comparisons; it shouldn't impact equality, as it's not user-surfaced. @@ -72,31 +64,8 @@ pub trait LanguageToolchainStore: Send + Sync + 'static { ) -> Option; } -pub trait LocalLanguageToolchainStore: Send + Sync + 'static { - fn active_toolchain( - self: Arc, - worktree_id: WorktreeId, - relative_path: &Arc, - language_name: LanguageName, - cx: &mut AsyncApp, - ) -> Option; -} - -#[async_trait(?Send )] -impl LanguageToolchainStore for T { - async fn active_toolchain( - self: Arc, - worktree_id: WorktreeId, - relative_path: Arc, - language_name: LanguageName, - cx: &mut AsyncApp, - ) -> Option { - self.active_toolchain(worktree_id, &relative_path, language_name, cx) - } -} - type DefaultIndex = usize; -#[derive(Default, Clone, Debug)] +#[derive(Default, Clone)] pub struct ToolchainList { pub toolchains: Vec, pub default: Option, diff --git a/crates/language_extension/src/extension_lsp_adapter.rs b/crates/language_extension/src/extension_lsp_adapter.rs index e465a8dd0a..58fbe6cda2 100644 --- a/crates/language_extension/src/extension_lsp_adapter.rs +++ b/crates/language_extension/src/extension_lsp_adapter.rs @@ -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, LspAdapter, LspAdapterDelegate, - Toolchain, + BinaryStatus, CodeLabel, HighlightId, Language, LanguageName, LanguageToolchainStore, + LspAdapter, LspAdapterDelegate, }; use lsp::{ CodeActionKind, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerName, @@ -159,7 +159,7 @@ impl LspAdapter for ExtensionLspAdapter { fn get_language_server_command<'a>( self: Arc, delegate: Arc, - _: Option, + _: Arc, _: LanguageServerBinaryOptions, _: futures::lock::MutexGuard<'a, Option>, _: &'a mut AsyncApp, @@ -242,7 +242,7 @@ impl LspAdapter for ExtensionLspAdapter { ])) } - fn language_ids(&self) -> HashMap { + fn language_ids(&self) -> HashMap { // TODO: The language IDs can be provided via the language server options // in `extension.toml now but we're leaving these existing usages in place temporarily // to avoid any compatibility issues between Zed and the extension versions. @@ -250,7 +250,7 @@ impl LspAdapter for ExtensionLspAdapter { // We can remove once the following extension versions no longer see any use: // - php@0.0.1 if self.extension.manifest().id.as_ref() == "php" { - return HashMap::from_iter([(LanguageName::new("PHP"), "php".into())]); + return HashMap::from_iter([("PHP".into(), "php".into())]); } self.extension @@ -288,7 +288,7 @@ impl LspAdapter for ExtensionLspAdapter { self: Arc, _: &dyn Fs, delegate: &Arc, - _: Option, + _: Arc, _cx: &mut AsyncApp, ) -> Result { let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _; @@ -336,7 +336,7 @@ impl LspAdapter for ExtensionLspAdapter { target_language_server_id: LanguageServerName, _: &dyn Fs, delegate: &Arc, - + _: Arc, _cx: &mut AsyncApp, ) -> Result> { let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _; diff --git a/crates/language_extension/src/language_extension.rs b/crates/language_extension/src/language_extension.rs index 510f870ce8..1915eae2d1 100644 --- a/crates/language_extension/src/language_extension.rs +++ b/crates/language_extension/src/language_extension.rs @@ -52,7 +52,7 @@ impl ExtensionLanguageProxy for LanguageServerRegistryProxy { load: Arc Result + Send + Sync + 'static>, ) { self.language_registry - .register_language(language, grammar, matcher, hidden, None, load); + .register_language(language, grammar, matcher, hidden, load); } fn remove_languages( @@ -61,6 +61,6 @@ impl ExtensionLanguageProxy for LanguageServerRegistryProxy { grammars_to_remove: &[Arc], ) { self.language_registry - .remove_languages(languages_to_remove, grammars_to_remove); + .remove_languages(&languages_to_remove, &grammars_to_remove); } } diff --git a/crates/language_model/Cargo.toml b/crates/language_model/Cargo.toml index f9920623b5..841be60b0e 100644 --- a/crates/language_model/Cargo.toml +++ b/crates/language_model/Cargo.toml @@ -20,7 +20,6 @@ anthropic = { workspace = true, features = ["schemars"] } anyhow.workspace = true base64.workspace = true client.workspace = true -cloud_api_types.workspace = true cloud_llm_client.workspace = true collections.workspace = true futures.workspace = true diff --git a/crates/language_model/src/fake_provider.rs b/crates/language_model/src/fake_provider.rs index b06a475f93..d54db7554a 100644 --- a/crates/language_model/src/fake_provider.rs +++ b/crates/language_model/src/fake_provider.rs @@ -1,19 +1,14 @@ use crate::{ - AuthenticateError, ConfigurationViewTargetAgent, LanguageModel, LanguageModelCompletionError, - LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider, - LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState, - LanguageModelRequest, LanguageModelToolChoice, + AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, + LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId, + LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest, + LanguageModelToolChoice, }; -use anyhow::anyhow; -use futures::{FutureExt, channel::mpsc, future::BoxFuture, stream::BoxStream}; +use futures::{FutureExt, StreamExt, channel::mpsc, future::BoxFuture, stream::BoxStream}; use gpui::{AnyView, App, AsyncApp, Entity, Task, Window}; use http_client::Result; use parking_lot::Mutex; -use smol::stream::StreamExt; -use std::sync::{ - Arc, - atomic::{AtomicBool, Ordering::SeqCst}, -}; +use std::sync::Arc; #[derive(Clone)] pub struct FakeLanguageModelProvider { @@ -67,12 +62,7 @@ impl LanguageModelProvider for FakeLanguageModelProvider { Task::ready(Ok(())) } - fn configuration_view( - &self, - _target_agent: ConfigurationViewTargetAgent, - _window: &mut Window, - _: &mut App, - ) -> AnyView { + fn configuration_view(&self, _window: &mut Window, _: &mut App) -> AnyView { unimplemented!() } @@ -102,15 +92,7 @@ pub struct ToolUseRequest { pub struct FakeLanguageModel { provider_id: LanguageModelProviderId, provider_name: LanguageModelProviderName, - current_completion_txs: Mutex< - Vec<( - LanguageModelRequest, - mpsc::UnboundedSender< - Result, - >, - )>, - >, - forbid_requests: AtomicBool, + current_completion_txs: Mutex)>>, } impl Default for FakeLanguageModel { @@ -119,20 +101,11 @@ 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 { self.current_completion_txs .lock() @@ -145,21 +118,10 @@ impl FakeLanguageModel { self.current_completion_txs.lock().len() } - pub fn send_completion_stream_text_chunk( + pub fn stream_completion_response( &self, request: &LanguageModelRequest, chunk: impl Into, - ) { - self.send_completion_stream_event( - request, - LanguageModelCompletionEvent::Text(chunk.into()), - ); - } - - pub fn send_completion_stream_event( - &self, - request: &LanguageModelRequest, - event: impl Into, ) { let current_completion_txs = self.current_completion_txs.lock(); let tx = current_completion_txs @@ -167,21 +129,7 @@ impl FakeLanguageModel { .find(|(req, _)| req == request) .map(|(_, tx)| tx) .unwrap(); - tx.unbounded_send(Ok(event.into())).unwrap(); - } - - pub fn send_completion_stream_error( - &self, - request: &LanguageModelRequest, - error: impl Into, - ) { - 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(); + tx.unbounded_send(chunk.into()).unwrap(); } pub fn end_completion_stream(&self, request: &LanguageModelRequest) { @@ -190,22 +138,8 @@ impl FakeLanguageModel { .retain(|(req, _)| req != request); } - pub fn send_last_completion_stream_text_chunk(&self, chunk: impl Into) { - self.send_completion_stream_text_chunk(self.pending_completions().last().unwrap(), chunk); - } - - pub fn send_last_completion_stream_event( - &self, - event: impl Into, - ) { - self.send_completion_stream_event(self.pending_completions().last().unwrap(), event); - } - - pub fn send_last_completion_stream_error( - &self, - error: impl Into, - ) { - self.send_completion_stream_error(self.pending_completions().last().unwrap(), error); + pub fn stream_last_completion_response(&self, chunk: impl Into) { + self.stream_completion_response(self.pending_completions().last().unwrap(), chunk); } pub fn end_last_completion_stream(&self) { @@ -265,18 +199,14 @@ impl LanguageModel for FakeLanguageModel { LanguageModelCompletionError, >, > { - 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() + let (tx, rx) = mpsc::unbounded(); + self.current_completion_txs.lock().push((request, tx)); + async move { + Ok(rx + .map(|text| Ok(LanguageModelCompletionEvent::Text(text))) + .boxed()) } + .boxed() } fn as_fake(&self) -> &Self { diff --git a/crates/language_model/src/language_model.rs b/crates/language_model/src/language_model.rs index d5313b6a3a..1637d2de8a 100644 --- a/crates/language_model/src/language_model.rs +++ b/crates/language_model/src/language_model.rs @@ -14,7 +14,7 @@ use client::Client; use cloud_llm_client::{CompletionMode, CompletionRequestStatus}; use futures::FutureExt; use futures::{StreamExt, future::BoxFuture, stream::BoxStream}; -use gpui::{AnyView, App, AsyncApp, SharedString, Task, Window}; +use gpui::{AnyElement, AnyView, App, AsyncApp, SharedString, Task, Window}; use http_client::{StatusCode, http}; use icons::IconName; use parking_lot::Mutex; @@ -54,7 +54,7 @@ pub const ZED_CLOUD_PROVIDER_NAME: LanguageModelProviderName = pub fn init(client: Arc, cx: &mut App) { init_settings(cx); - RefreshLlmTokenListener::register(client, cx); + RefreshLlmTokenListener::register(client.clone(), cx); } pub fn init_settings(cx: &mut App) { @@ -300,7 +300,7 @@ impl From for LanguageModelCompletionError { }, AnthropicError::ServerOverloaded { retry_after } => Self::ServerOverloaded { provider, - retry_after, + retry_after: retry_after, }, AnthropicError::ApiError(api_error) => api_error.into(), } @@ -538,7 +538,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); + message_id = Some(id.clone()); } Ok(LanguageModelCompletionEvent::Text(text)) => { first_item_text = Some(text); @@ -634,22 +634,20 @@ pub trait LanguageModelProvider: 'static { } fn is_authenticated(&self, cx: &App) -> bool; fn authenticate(&self, cx: &mut App) -> Task>; - fn configuration_view( + fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView; + fn must_accept_terms(&self, _cx: &App) -> bool { + false + } + fn render_accept_terms( &self, - target_agent: ConfigurationViewTargetAgent, - window: &mut Window, - cx: &mut App, - ) -> AnyView; + _view: LanguageModelProviderTosView, + _cx: &mut App, + ) -> Option { + None + } fn reset_credentials(&self, cx: &mut App) -> Task>; } -#[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. diff --git a/crates/language_model/src/model/cloud_model.rs b/crates/language_model/src/model/cloud_model.rs index 8a7f3456fb..a5d2ac34f5 100644 --- a/crates/language_model/src/model/cloud_model.rs +++ b/crates/language_model/src/model/cloud_model.rs @@ -3,9 +3,10 @@ use std::sync::Arc; use anyhow::Result; use client::Client; -use cloud_api_types::websocket_protocol::MessageToClient; -use cloud_llm_client::Plan; -use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Global, ReadGlobal as _}; +use gpui::{ + App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Global, ReadGlobal as _, +}; +use proto::{Plan, TypedEnvelope}; use smol::lock::{RwLock, RwLockUpgradableReadGuard, RwLockWriteGuard}; use thiserror::Error; @@ -29,7 +30,7 @@ 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::Free => "Model request limit reached. Upgrade to Zed Pro for more requests.", Plan::ZedPro => { "Model request limit reached. Upgrade to usage-based billing for more requests." } @@ -42,18 +43,6 @@ impl fmt::Display for ModelRequestLimitReachedError { } } -#[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>>); @@ -82,7 +71,7 @@ impl LlmApiToken { let response = client.cloud_client().create_llm_token(system_id).await?; *lock = Some(response.token.0.clone()); - Ok(response.token.0) + Ok(response.token.0.clone()) } } @@ -92,7 +81,9 @@ impl Global for GlobalRefreshLlmTokenListener {} pub struct RefreshLlmTokenEvent; -pub struct RefreshLlmTokenListener; +pub struct RefreshLlmTokenListener { + _llm_token_subscription: client::Subscription, +} impl EventEmitter for RefreshLlmTokenListener {} @@ -107,21 +98,17 @@ impl RefreshLlmTokenListener { } fn new(client: Arc, cx: &mut Context) -> Self { - client.add_message_to_client_handler({ - let this = cx.entity(); - move |message, cx| { - Self::handle_refresh_llm_token(this.clone(), message, cx); - } - }); - - Self - } - - fn handle_refresh_llm_token(this: Entity, message: &MessageToClient, cx: &mut App) { - match message { - MessageToClient::UserUpdated => { - this.update(cx, |_this, cx| cx.emit(RefreshLlmTokenEvent)); - } + Self { + _llm_token_subscription: client + .add_message_handler(cx.weak_entity(), Self::handle_refresh_llm_token), } } + + async fn handle_refresh_llm_token( + this: Entity, + _: TypedEnvelope, + mut cx: AsyncApp, + ) -> Result<()> { + this.update(&mut cx, |_this, cx| cx.emit(RefreshLlmTokenEvent)) + } } diff --git a/crates/language_model/src/registry.rs b/crates/language_model/src/registry.rs index 531c3615dc..7cf071808a 100644 --- a/crates/language_model/src/registry.rs +++ b/crates/language_model/src/registry.rs @@ -21,10 +21,13 @@ 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), + #[error("Using the {} LLM provider requires accepting the Terms of Service.", + .0.name().0)] + ProviderPendingTermsAcceptance(Arc), } impl std::fmt::Debug for ConfigurationError { @@ -35,6 +38,9 @@ impl std::fmt::Debug for ConfigurationError { Self::ProviderNotAuthenticated(provider) => { write!(f, "ProviderNotAuthenticated({})", provider.id()) } + Self::ProviderPendingTermsAcceptance(provider) => { + write!(f, "ProviderPendingTermsAcceptance({})", provider.id()) + } } } } @@ -101,7 +107,7 @@ pub enum Event { InlineAssistantModelChanged, CommitMessageModelChanged, ThreadSummaryModelChanged, - ProviderStateChanged(LanguageModelProviderId), + ProviderStateChanged, AddedProvider(LanguageModelProviderId), RemovedProvider(LanguageModelProviderId), } @@ -142,11 +148,8 @@ impl LanguageModelRegistry { ) { let id = provider.id(); - let subscription = provider.subscribe(cx, { - let id = id.clone(); - move |_, cx| { - cx.emit(Event::ProviderStateChanged(id.clone())); - } + let subscription = provider.subscribe(cx, |_, cx| { + cx.emit(Event::ProviderStateChanged); }); if let Some(subscription) = subscription { subscription.detach(); @@ -194,6 +197,12 @@ impl LanguageModelRegistry { return Some(ConfigurationError::ProviderNotAuthenticated(model.provider)); } + if model.provider.must_accept_terms(cx) { + return Some(ConfigurationError::ProviderPendingTermsAcceptance( + model.provider, + )); + } + None } diff --git a/crates/language_model/src/request.rs b/crates/language_model/src/request.rs index 1182e0f7a8..dc485e9937 100644 --- a/crates/language_model/src/request.rs +++ b/crates/language_model/src/request.rs @@ -220,39 +220,42 @@ 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")) - && let Some(type_str) = type_value.as_str() - && type_str.to_lowercase() == "text" - && let Some(text) = text_value.as_str() + (get_field(&obj, "type"), get_field(&obj, "text")) { - return Ok(Self::Text(Arc::from(text))); + 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))); + } + } + } } // Check for wrapped Text variant: { "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))); + 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))); + } } } // Check for wrapped Image variant: { "image": { "source": "...", "size": ... } } - 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)); + 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)); + } + } } } // 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)); } } @@ -269,7 +272,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, } } @@ -294,12 +297,6 @@ impl From for LanguageModelToolResultContent { } } -impl From for LanguageModelToolResultContent { - fn from(image: LanguageModelImage) -> Self { - Self::Image(image) - } -} - #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash)] pub enum MessageContent { Text(String), diff --git a/crates/language_model/src/role.rs b/crates/language_model/src/role.rs index 4b47ef36dd..953dfa6fdf 100644 --- a/crates/language_model/src/role.rs +++ b/crates/language_model/src/role.rs @@ -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, diff --git a/crates/language_models/Cargo.toml b/crates/language_models/Cargo.toml index b5bfb870f6..208b0d99c9 100644 --- a/crates/language_models/Cargo.toml +++ b/crates/language_models/Cargo.toml @@ -44,6 +44,7 @@ ollama = { workspace = true, features = ["schemars"] } open_ai = { workspace = true, features = ["schemars"] } open_router = { workspace = true, features = ["schemars"] } partial-json-fixer.workspace = true +proto.workspace = true release_channel.workspace = true schemars.workspace = true serde.workspace = true diff --git a/crates/language_models/src/language_models.rs b/crates/language_models/src/language_models.rs index 738b72b0c9..a88f12283a 100644 --- a/crates/language_models/src/language_models.rs +++ b/crates/language_models/src/language_models.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use ::settings::{Settings, SettingsStore}; -use client::{Client, UserStore}; +use client::{Client, CloudUserStore, UserStore}; use collections::HashSet; use gpui::{App, Context, Entity}; use language_model::{LanguageModelProviderId, LanguageModelRegistry}; @@ -26,11 +26,22 @@ use crate::provider::vercel::VercelLanguageModelProvider; use crate::provider::x_ai::XAiLanguageModelProvider; pub use crate::settings::*; -pub fn init(user_store: Entity, client: Arc, cx: &mut App) { +pub fn init( + user_store: Entity, + cloud_user_store: Entity, + client: Arc, + cx: &mut App, +) { crate::settings::init_settings(cx); let registry = LanguageModelRegistry::global(cx); registry.update(cx, |registry, cx| { - register_language_model_providers(registry, user_store, client.clone(), cx); + register_language_model_providers( + registry, + user_store, + cloud_user_store, + client.clone(), + cx, + ); }); let mut openai_compatible_providers = AllLanguageModelSettings::get_global(cx) @@ -100,11 +111,17 @@ fn register_openai_compatible_providers( fn register_language_model_providers( registry: &mut LanguageModelRegistry, user_store: Entity, + cloud_user_store: Entity, client: Arc, cx: &mut Context, ) { registry.register_provider( - CloudLanguageModelProvider::new(user_store, client.clone(), cx), + CloudLanguageModelProvider::new( + user_store.clone(), + cloud_user_store.clone(), + client.clone(), + cx, + ), cx, ); diff --git a/crates/language_models/src/provider/anthropic.rs b/crates/language_models/src/provider/anthropic.rs index c492edeaf5..959cbccf39 100644 --- a/crates/language_models/src/provider/anthropic.rs +++ b/crates/language_models/src/provider/anthropic.rs @@ -15,11 +15,11 @@ use gpui::{ }; use http_client::HttpClient; use language_model::{ - AuthenticateError, ConfigurationViewTargetAgent, LanguageModel, - LanguageModelCacheConfiguration, LanguageModelCompletionError, LanguageModelId, - LanguageModelName, LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName, - LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice, - LanguageModelToolResultContent, MessageContent, RateLimiter, Role, + AuthenticateError, 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,14 +153,29 @@ impl State { return Task::ready(Ok(())); } - let key = AnthropicLanguageModelProvider::api_key(cx); + let credentials_provider = ::global(cx); + let api_url = AllLanguageModelSettings::get_global(cx) + .anthropic + .api_url + .clone(); cx.spawn(async move |this, cx| { - let key = key.await?; + 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, + ) + }; this.update(cx, |this, cx| { - this.api_key = Some(key.key); - this.api_key_from_env = key.from_env; + this.api_key = Some(api_key); + this.api_key_from_env = from_env; cx.notify(); })?; @@ -169,11 +184,6 @@ impl State { } } -pub struct ApiKey { - pub key: String, - pub from_env: bool, -} - impl AnthropicLanguageModelProvider { pub fn new(http_client: Arc, cx: &mut App) -> Self { let state = cx.new(|cx| State { @@ -196,33 +206,6 @@ impl AnthropicLanguageModelProvider { request_limiter: RateLimiter::new(4), }) } - - pub fn api_key(cx: &mut App) -> Task> { - let credentials_provider = ::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 { @@ -316,13 +299,8 @@ impl LanguageModelProvider for AnthropicLanguageModelProvider { self.state.update(cx, |state, cx| state.authenticate(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)) + fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView { + cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx)) .into() } @@ -554,7 +532,7 @@ pub fn into_anthropic( .into_iter() .filter_map(|content| match content { MessageContent::Text(text) => { - let text = if text.chars().last().is_some_and(|c| c.is_whitespace()) { + let text = if text.chars().last().map_or(false, |c| c.is_whitespace()) { text.trim_end().to_string() } else { text @@ -633,11 +611,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() - && last_message.role == anthropic_role - { - last_message.content.extend(anthropic_message_content); - continue; + if let Some(last_message) = new_messages.last_mut() { + if last_message.role == anthropic_role { + last_message.content.extend(anthropic_message_content); + continue; + } } // Mark the last segment of the message as cached @@ -813,7 +791,7 @@ impl AnthropicEventMapper { ))]; } } - vec![] + return vec![]; } }, Event::ContentBlockStop { index } => { @@ -924,18 +902,12 @@ struct ConfigurationView { api_key_editor: Entity, state: gpui::Entity, load_credentials_task: Option>, - target_agent: ConfigurationViewTargetAgent, } impl ConfigurationView { const PLACEHOLDER_TEXT: &'static str = "sk-ant-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; - fn new( - state: gpui::Entity, - target_agent: ConfigurationViewTargetAgent, - window: &mut Window, - cx: &mut Context, - ) -> Self { + fn new(state: gpui::Entity, window: &mut Window, cx: &mut Context) -> Self { cx.observe(&state, |_, _, cx| { cx.notify(); }) @@ -967,7 +939,6 @@ impl ConfigurationView { }), state, load_credentials_task, - target_agent, } } @@ -1041,10 +1012,7 @@ impl Render for ConfigurationView { v_flex() .size_full() .on_action(cx.listener(Self::save_api_key)) - .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(Label::new("To use Zed's assistant with Anthropic, you need to add an API key. Follow these steps:")) .child( List::new() .child( @@ -1055,7 +1023,7 @@ impl Render for ConfigurationView { ) ) .child( - InstructionListItem::text_only("Paste your API key below and hit enter to start using the agent") + InstructionListItem::text_only("Paste your API key below and hit enter to start using the assistant") ) ) .child( diff --git a/crates/language_models/src/provider/bedrock.rs b/crates/language_models/src/provider/bedrock.rs index 178c767950..a86b3e78f5 100644 --- a/crates/language_models/src/provider/bedrock.rs +++ b/crates/language_models/src/provider/bedrock.rs @@ -150,7 +150,7 @@ impl State { let credentials_provider = ::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,12 +348,7 @@ impl LanguageModelProvider for BedrockLanguageModelProvider { self.state.update(cx, |state, cx| state.authenticate(cx)) } - fn configuration_view( - &self, - _target_agent: language_model::ConfigurationViewTargetAgent, - window: &mut Window, - cx: &mut App, - ) -> AnyView { + fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView { cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx)) .into() } @@ -412,10 +407,10 @@ impl BedrockModel { .region(Region::new(region)) .timeout_config(TimeoutConfig::disabled()); - if let Some(endpoint_url) = endpoint - && !endpoint_url.is_empty() - { - config_builder = config_builder.endpoint_url(endpoint_url); + if let Some(endpoint_url) = endpoint { + if !endpoint_url.is_empty() { + config_builder = config_builder.endpoint_url(endpoint_url); + } } match auth_method { @@ -465,7 +460,7 @@ impl BedrockModel { Result>>, > { let Ok(runtime_client) = self - .get_or_init_client(cx) + .get_or_init_client(&cx) .cloned() .context("Bedrock client not initialized") else { @@ -728,11 +723,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() - && last_message.role == bedrock_role - { - last_message.content.extend(bedrock_message_content); - continue; + if let Some(last_message) = new_messages.last_mut() { + if last_message.role == bedrock_role { + last_message.content.extend(bedrock_message_content); + continue; + } } new_messages.push( BedrockMessage::builder() @@ -917,7 +912,7 @@ pub fn map_to_language_model_completion_events( Some(ContentBlockDelta::ReasoningContent(thinking)) => match thinking { ReasoningContentBlockDelta::Text(thoughts) => { Some(Ok(LanguageModelCompletionEvent::Thinking { - text: thoughts, + text: thoughts.clone(), signature: None, })) } @@ -968,7 +963,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, + raw_input: tool_use.input_json.clone(), input, }, )) @@ -1086,18 +1081,21 @@ 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() { @@ -1105,7 +1103,13 @@ impl ConfigurationView { } else { Some(session_token) }; - let region = self.region_editor.read(cx).text(cx).trim().to_string(); + let region = self + .region_editor + .read(cx) + .text(cx) + .to_string() + .trim() + .to_string(); let region = if region.is_empty() { "us-east-1".to_string() } else { @@ -1247,7 +1251,7 @@ impl Render for ConfigurationView { v_flex() .size_full() .on_action(cx.listener(ConfigurationView::save_credentials)) - .child(Label::new("To use Zed's agent with Bedrock, you can set a custom authentication strategy through the settings.json, or use static credentials.")) + .child(Label::new("To use Zed's assistant with Bedrock, you can set a custom authentication strategy through the settings.json, or use static credentials.")) .child(Label::new("But, to access models on AWS, you need to:").mt_1()) .child( List::new() diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index b473d06357..a5de7f3442 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -2,7 +2,7 @@ use ai_onboarding::YoungAccountBanner; use anthropic::AnthropicModelMode; use anyhow::{Context as _, Result, anyhow}; use chrono::{DateTime, Utc}; -use client::{Client, ModelRequestUsage, UserStore, zed_urls}; +use client::{Client, CloudUserStore, ModelRequestUsage, UserStore, zed_urls}; use cloud_llm_client::{ CLIENT_SUPPORTS_STATUS_MESSAGES_HEADER_NAME, CURRENT_PLAN_HEADER_NAME, CompletionBody, CompletionEvent, CompletionRequestStatus, CountTokensBody, CountTokensResponse, @@ -23,9 +23,9 @@ use language_model::{ AuthenticateError, LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName, - LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice, - LanguageModelToolSchemaFormat, LlmApiToken, ModelRequestLimitReachedError, - PaymentRequiredError, RateLimiter, RefreshLlmTokenListener, + LanguageModelProviderState, LanguageModelProviderTosView, LanguageModelRequest, + LanguageModelToolChoice, LanguageModelToolSchemaFormat, LlmApiToken, + ModelRequestLimitReachedError, PaymentRequiredError, RateLimiter, RefreshLlmTokenListener, }; use release_channel::AppVersion; use schemars::JsonSchema; @@ -117,7 +117,9 @@ pub struct State { client: Arc, llm_api_token: LlmApiToken, user_store: Entity, + cloud_user_store: Entity, status: client::Status, + accept_terms_of_service_task: Option>>, models: Vec>, default_model: Option>, default_fast_model: Option>, @@ -131,33 +133,50 @@ impl State { fn new( client: Arc, user_store: Entity, + cloud_user_store: Entity, status: client::Status, cx: &mut Context, ) -> Self { let refresh_llm_token_listener = RefreshLlmTokenListener::global(cx); - let mut current_user = user_store.read(cx).watch_current_user(); + Self { client: client.clone(), llm_api_token: LlmApiToken::default(), user_store, + cloud_user_store, status, + accept_terms_of_service_task: None, models: Vec::new(), default_model: None, default_fast_model: None, recommended_models: Vec::new(), _fetch_models_task: cx.spawn(async move |this, cx| { maybe!(async move { - let (client, llm_api_token) = this - .read_with(cx, |this, _cx| (client.clone(), this.llm_api_token.clone()))?; + let (client, cloud_user_store, llm_api_token) = + this.read_with(cx, |this, _cx| { + ( + client.clone(), + this.cloud_user_store.clone(), + this.llm_api_token.clone(), + ) + })?; - while current_user.borrow().is_none() { - current_user.next().await; + loop { + let is_authenticated = + cloud_user_store.read_with(cx, |this, _cx| this.is_authenticated())?; + if is_authenticated { + break; + } + + cx.background_executor() + .timer(Duration::from_millis(100)) + .await; } - let response = - Self::fetch_models(client.clone(), llm_api_token.clone()).await?; - this.update(cx, |this, cx| this.update_models(response, cx))?; - anyhow::Ok(()) + let response = Self::fetch_models(client, llm_api_token).await?; + this.update(cx, |this, cx| { + this.update_models(response, cx); + }) }) .await .context("failed to fetch Zed models") @@ -185,16 +204,37 @@ impl State { } fn is_signed_out(&self, cx: &App) -> bool { - self.user_store.read(cx).current_user().is_none() + !self.cloud_user_store.read(cx).is_authenticated() } fn authenticate(&self, cx: &mut Context) -> Task> { let client = self.client.clone(); cx.spawn(async move |state, cx| { - client.sign_in_with_optional_connect(true, cx).await?; + client + .authenticate_and_connect(true, &cx) + .await + .into_response()?; state.update(cx, |_, cx| cx.notify()) }) } + + fn has_accepted_terms_of_service(&self, cx: &App) -> bool { + self.cloud_user_store.read(cx).has_accepted_tos() + } + + fn accept_terms_of_service(&mut self, cx: &mut Context) { + 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) { let mut models = Vec::new(); @@ -250,7 +290,7 @@ impl State { if response.status().is_success() { let mut body = String::new(); response.body_mut().read_to_string(&mut body).await?; - Ok(serde_json::from_str(&body)?) + return Ok(serde_json::from_str(&body)?); } else { let mut body = String::new(); response.body_mut().read_to_string(&mut body).await?; @@ -263,11 +303,24 @@ impl State { } impl CloudLanguageModelProvider { - pub fn new(user_store: Entity, client: Arc, cx: &mut App) -> Self { + pub fn new( + user_store: Entity, + cloud_user_store: Entity, + client: Arc, + cx: &mut App, + ) -> Self { let mut status_rx = client.status(); let status = *status_rx.borrow(); - let state = cx.new(|cx| State::new(client.clone(), user_store.clone(), status, cx)); + let state = cx.new(|cx| { + State::new( + client.clone(), + user_store.clone(), + cloud_user_store.clone(), + status, + cx, + ) + }); let state_ref = state.downgrade(); let maintain_client_status = cx.spawn(async move |cx| { @@ -287,7 +340,7 @@ impl CloudLanguageModelProvider { Self { client, - state, + state: state.clone(), _maintain_client_status: maintain_client_status, } } @@ -300,7 +353,7 @@ impl CloudLanguageModelProvider { Arc::new(CloudLanguageModel { id: LanguageModelId(SharedString::from(model.id.0.clone())), model, - llm_api_token, + llm_api_token: llm_api_token.clone(), client: self.client.clone(), request_limiter: RateLimiter::new(4), }) @@ -364,28 +417,124 @@ impl LanguageModelProvider for CloudLanguageModelProvider { fn is_authenticated(&self, cx: &App) -> bool { let state = self.state.read(cx); - !state.is_signed_out(cx) + !state.is_signed_out(cx) && state.has_accepted_terms_of_service(cx) } fn authenticate(&self, _cx: &mut App) -> Task> { Task::ready(Ok(())) } - fn configuration_view( - &self, - _target_agent: language_model::ConfigurationViewTargetAgent, - _: &mut Window, - cx: &mut App, - ) -> AnyView { + 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( + &self, + view: LanguageModelProviderTosView, + cx: &mut App, + ) -> Option { + 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(), + ) + } + fn reset_credentials(&self, _cx: &mut App) -> Task> { Task::ready(Ok(())) } } +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::XSmall) + .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, @@ -476,13 +625,20 @@ impl CloudLanguageModel { .headers() .get(SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME) .and_then(|resource| resource.to_str().ok()) - && let Some(plan) = response + { + if 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 })); + { + let plan = match 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, + }; + return Err(anyhow!(ModelRequestLimitReachedError { plan })); + } } } else if status == StatusCode::PAYMENT_REQUIRED { return Err(anyhow!(PaymentRequiredError)); @@ -539,29 +695,29 @@ where impl From for LanguageModelCompletionError { fn from(error: ApiError) -> Self { - if let Ok(cloud_error) = serde_json::from_str::(&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::().ok()) - .and_then(|code| StatusCode::from_u16(code).ok()) - .unwrap_or(error.status) - }; + if let Ok(cloud_error) = serde_json::from_str::(&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::().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; @@ -823,8 +979,6 @@ impl LanguageModel for CloudLanguageModel { request, model.id(), model.supports_parallel_tool_calls(), - model.supports_prompt_cache_key(), - None, None, ); let llm_api_token = self.llm_api_token.clone(); @@ -986,7 +1140,10 @@ struct ZedAiConfiguration { plan: Option, subscription_period: Option<(DateTime, DateTime)>, 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, sign_in_callback: Arc, } @@ -1052,30 +1209,58 @@ impl RenderOnce for ZedAiConfiguration { ); } - 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) - } - }) + 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) } } struct ConfigurationView { state: Entity, + accept_terms_of_service_callback: Arc, sign_in_callback: Arc, } impl ConfigurationView { fn new(state: Entity) -> 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| { @@ -1087,6 +1272,7 @@ impl ConfigurationView { Self { state, + accept_terms_of_service_callback, sign_in_callback, } } @@ -1095,30 +1281,25 @@ impl ConfigurationView { impl Render for ConfigurationView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let state = self.state.read(cx); - let user_store = state.user_store.read(cx); + let cloud_user_store = state.cloud_user_store.read(cx); ZedAiConfiguration { is_connected: !state.is_signed_out(cx), - plan: user_store.plan(), - subscription_period: user_store.subscription_period(), - eligible_for_trial: user_store.trial_started_at().is_none(), - account_too_young: user_store.account_too_young(), + plan: cloud_user_store.plan(), + subscription_period: cloud_user_store.subscription_period(), + eligible_for_trial: cloud_user_store.trial_started_at().is_none(), + has_accepted_terms_of_service: state.has_accepted_terms_of_service(cx), + account_too_young: cloud_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(), } } } impl Component for ZedAiConfiguration { - fn name() -> &'static str { - "AI Configuration Content" - } - - fn sort_name() -> &'static str { - "AI Configuration Content" - } - fn scope() -> ComponentScope { - ComponentScope::Onboarding + ComponentScope::Agent } fn preview(_window: &mut Window, _cx: &mut App) -> Option { @@ -1127,6 +1308,7 @@ impl Component for ZedAiConfiguration { plan: Option, eligible_for_trial: bool, account_too_young: bool, + has_accepted_terms_of_service: bool, ) -> AnyElement { ZedAiConfiguration { is_connected, @@ -1135,7 +1317,10 @@ 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() @@ -1146,30 +1331,33 @@ impl Component for ZedAiConfiguration { .p_4() .gap_4() .children(vec![ - single_example("Not connected", configuration(false, None, false, false)), + single_example( + "Not connected", + configuration(false, None, false, false, true), + ), single_example( "Accept Terms of Service", - configuration(true, None, true, false), + configuration(true, None, true, false, false), ), single_example( "No Plan - Not eligible for trial", - configuration(true, None, false, false), + configuration(true, None, false, false, true), ), single_example( "No Plan - Eligible for trial", - configuration(true, None, true, false), + configuration(true, None, true, false, true), ), single_example( "Free Plan", - configuration(true, Some(Plan::ZedFree), true, false), + configuration(true, Some(Plan::ZedFree), true, false, true), ), single_example( "Zed Pro Trial Plan", - configuration(true, Some(Plan::ZedProTrial), true, false), + configuration(true, Some(Plan::ZedProTrial), true, false, true), ), single_example( "Zed Pro Plan", - configuration(true, Some(Plan::ZedPro), true, false), + configuration(true, Some(Plan::ZedPro), true, false, true), ), ]) .into_any_element(), diff --git a/crates/language_models/src/provider/copilot_chat.rs b/crates/language_models/src/provider/copilot_chat.rs index eb12c0056f..3cdc2e5401 100644 --- a/crates/language_models/src/provider/copilot_chat.rs +++ b/crates/language_models/src/provider/copilot_chat.rs @@ -176,12 +176,7 @@ impl LanguageModelProvider for CopilotChatLanguageModelProvider { Task::ready(Err(err.into())) } - fn configuration_view( - &self, - _target_agent: language_model::ConfigurationViewTargetAgent, - _: &mut Window, - cx: &mut App, - ) -> AnyView { + fn configuration_view(&self, _: &mut Window, cx: &mut App) -> AnyView { let state = self.state.clone(); cx.new(|cx| ConfigurationView::new(state, cx)).into() } @@ -711,8 +706,7 @@ impl Render for ConfigurationView { .child(svg().size_8().path(IconName::CopilotError.path())) } _ => { - const LABEL: &str = "To use Zed's agent with GitHub Copilot, you need to be logged in to GitHub. Note that your GitHub account must have an active Copilot Chat subscription."; - + const LABEL: &str = "To use Zed's assistant with GitHub Copilot, you need to be logged in to GitHub. Note that your GitHub account must have an active Copilot Chat subscription."; v_flex().gap_2().child(Label::new(LABEL)).child( Button::new("sign_in", "Sign in to use GitHub Copilot") .icon_color(Color::Muted) diff --git a/crates/language_models/src/provider/deepseek.rs b/crates/language_models/src/provider/deepseek.rs index 8c7f8bcc35..a568ef4034 100644 --- a/crates/language_models/src/provider/deepseek.rs +++ b/crates/language_models/src/provider/deepseek.rs @@ -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,12 +229,7 @@ impl LanguageModelProvider for DeepSeekLanguageModelProvider { self.state.update(cx, |state, cx| state.authenticate(cx)) } - fn configuration_view( - &self, - _target_agent: language_model::ConfigurationViewTargetAgent, - window: &mut Window, - cx: &mut App, - ) -> AnyView { + fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView { cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx)) .into() } diff --git a/crates/language_models/src/provider/google.rs b/crates/language_models/src/provider/google.rs index f252ab7aa3..bd8a09970a 100644 --- a/crates/language_models/src/provider/google.rs +++ b/crates/language_models/src/provider/google.rs @@ -12,9 +12,9 @@ use gpui::{ }; use http_client::HttpClient; use language_model::{ - AuthenticateError, ConfigurationViewTargetAgent, LanguageModelCompletionError, - LanguageModelCompletionEvent, LanguageModelToolChoice, LanguageModelToolSchemaFormat, - LanguageModelToolUse, LanguageModelToolUseId, MessageContent, StopReason, + AuthenticateError, LanguageModelCompletionError, LanguageModelCompletionEvent, + LanguageModelToolChoice, LanguageModelToolSchemaFormat, LanguageModelToolUse, + LanguageModelToolUseId, MessageContent, StopReason, }; use language_model::{ LanguageModel, LanguageModelId, LanguageModelName, LanguageModelProvider, @@ -37,8 +37,6 @@ 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; @@ -112,7 +110,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 +129,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); @@ -158,7 +156,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)?; ( @@ -200,33 +198,6 @@ impl GoogleLanguageModelProvider { request_limiter: RateLimiter::new(4), }) } - - pub fn api_key(cx: &mut App) -> Task> { - let credentials_provider = ::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 { @@ -306,13 +277,8 @@ impl LanguageModelProvider for GoogleLanguageModelProvider { self.state.update(cx, |state, cx| state.authenticate(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)) + fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView { + cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx)) .into() } @@ -416,7 +382,7 @@ impl LanguageModel for GoogleLanguageModel { cx: &App, ) -> BoxFuture<'static, Result> { let model_id = self.model.request_id().to_string(); - let request = into_google(request, model_id, self.model.mode()); + let request = into_google(request, model_id.clone(), self.model.mode()); let http_client = self.http_client.clone(); let api_key = self.state.read(cx).api_key.clone(); @@ -559,7 +525,7 @@ pub fn into_google( let system_instructions = if request .messages .first() - .is_some_and(|msg| matches!(msg.role, Role::System)) + .map_or(false, |msg| matches!(msg.role, Role::System)) { let message = request.messages.remove(0); Some(SystemInstruction { @@ -606,7 +572,7 @@ pub fn into_google( top_k: None, }), safety_settings: None, - tools: (!request.tools.is_empty()).then(|| { + tools: (request.tools.len() > 0).then(|| { vec![google_ai::Tool { function_declarations: request .tools @@ -805,17 +771,11 @@ fn convert_usage(usage: &UsageMetadata) -> language_model::TokenUsage { struct ConfigurationView { api_key_editor: Entity, state: gpui::Entity, - target_agent: language_model::ConfigurationViewTargetAgent, load_credentials_task: Option>, } impl ConfigurationView { - fn new( - state: gpui::Entity, - target_agent: language_model::ConfigurationViewTargetAgent, - window: &mut Window, - cx: &mut Context, - ) -> Self { + fn new(state: gpui::Entity, window: &mut Window, cx: &mut Context) -> Self { cx.observe(&state, |_, _, cx| { cx.notify(); }) @@ -845,7 +805,6 @@ impl ConfigurationView { editor.set_placeholder_text("AIzaSy...", cx); editor }), - target_agent, state, load_credentials_task, } @@ -921,10 +880,7 @@ impl Render for ConfigurationView { v_flex() .size_full() .on_action(cx.listener(Self::save_api_key)) - .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(Label::new("To use Zed's assistant with Google AI, you need to add an API key. Follow these steps:")) .child( List::new() .child(InstructionListItem::new( diff --git a/crates/language_models/src/provider/lmstudio.rs b/crates/language_models/src/provider/lmstudio.rs index 80b28a396b..01600f3646 100644 --- a/crates/language_models/src/provider/lmstudio.rs +++ b/crates/language_models/src/provider/lmstudio.rs @@ -210,7 +210,7 @@ impl LanguageModelProvider for LmStudioLanguageModelProvider { .map(|model| { Arc::new(LmStudioLanguageModel { id: LanguageModelId::from(model.name.clone()), - model, + model: model.clone(), http_client: self.http_client.clone(), request_limiter: RateLimiter::new(4), }) as Arc @@ -226,12 +226,7 @@ impl LanguageModelProvider for LmStudioLanguageModelProvider { self.state.update(cx, |state, cx| state.authenticate(cx)) } - fn configuration_view( - &self, - _target_agent: language_model::ConfigurationViewTargetAgent, - _window: &mut Window, - cx: &mut App, - ) -> AnyView { + fn configuration_view(&self, _window: &mut Window, cx: &mut App) -> AnyView { let state = self.state.clone(); cx.new(|cx| ConfigurationView::new(state, cx)).into() } @@ -695,7 +690,7 @@ impl Render for ConfigurationView { Button::new("lmstudio-site", "LM Studio") .style(ButtonStyle::Subtle) .icon(IconName::ArrowUpRight) - .icon_size(IconSize::Small) + .icon_size(IconSize::XSmall) .icon_color(Color::Muted) .on_click(move |_, _window, cx| { cx.open_url(LMSTUDIO_SITE) @@ -710,7 +705,7 @@ impl Render for ConfigurationView { ) .style(ButtonStyle::Subtle) .icon(IconName::ArrowUpRight) - .icon_size(IconSize::Small) + .icon_size(IconSize::XSmall) .icon_color(Color::Muted) .on_click(move |_, _window, cx| { cx.open_url(LMSTUDIO_DOWNLOAD_URL) @@ -723,7 +718,7 @@ impl Render for ConfigurationView { Button::new("view-models", "Model Catalog") .style(ButtonStyle::Subtle) .icon(IconName::ArrowUpRight) - .icon_size(IconSize::Small) + .icon_size(IconSize::XSmall) .icon_color(Color::Muted) .on_click(move |_, _window, cx| { cx.open_url(LMSTUDIO_CATALOG_URL) @@ -749,7 +744,7 @@ impl Render for ConfigurationView { Button::new("retry_lmstudio_models", "Connect") .icon_position(IconPosition::Start) .icon_size(IconSize::XSmall) - .icon(IconName::PlayFilled) + .icon(IconName::Play) .on_click(cx.listener(move |this, _, _window, cx| { this.retry_connection(cx) })), diff --git a/crates/language_models/src/provider/mistral.rs b/crates/language_models/src/provider/mistral.rs index 3f8c2e2a67..fb385308fa 100644 --- a/crates/language_models/src/provider/mistral.rs +++ b/crates/language_models/src/provider/mistral.rs @@ -47,7 +47,6 @@ pub struct AvailableModel { pub max_completion_tokens: Option, pub supports_tools: Option, pub supports_images: Option, - pub supports_thinking: Option, } pub struct MistralLanguageModelProvider { @@ -76,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| { @@ -95,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?; this.update(cx, |this, cx| { this.api_key = Some(api_key); @@ -119,7 +118,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)?; ( @@ -216,7 +215,6 @@ impl LanguageModelProvider for MistralLanguageModelProvider { max_completion_tokens: model.max_completion_tokens, supports_tools: model.supports_tools, supports_images: model.supports_images, - supports_thinking: model.supports_thinking, }, ); } @@ -243,12 +241,7 @@ impl LanguageModelProvider for MistralLanguageModelProvider { self.state.update(cx, |state, cx| state.authenticate(cx)) } - fn configuration_view( - &self, - _target_agent: language_model::ConfigurationViewTargetAgent, - window: &mut Window, - cx: &mut App, - ) -> AnyView { + fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView { cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx)) .into() } @@ -373,7 +366,11 @@ impl LanguageModel for MistralLanguageModel { LanguageModelCompletionError, >, > { - let request = into_mistral(request, self.model.clone(), self.max_output_tokens()); + let request = into_mistral( + request, + self.model.id().to_string(), + self.max_output_tokens(), + ); let stream = self.stream_completion(request, cx); async move { @@ -387,7 +384,7 @@ impl LanguageModel for MistralLanguageModel { pub fn into_mistral( request: LanguageModelRequest, - model: mistral::Model, + model: String, max_output_tokens: Option, ) -> mistral::Request { let stream = true; @@ -404,20 +401,13 @@ pub fn into_mistral( .push_part(mistral::MessagePart::Text { text: text.clone() }); } MessageContent::Image(image_content) => { - if model.supports_images() { - message_content.push_part(mistral::MessagePart::ImageUrl { - image_url: image_content.to_base64_url(), - }); - } + message_content.push_part(mistral::MessagePart::ImageUrl { + image_url: image_content.to_base64_url(), + }); } MessageContent::Thinking { text, .. } => { - if model.supports_thinking() { - message_content.push_part(mistral::MessagePart::Thinking { - thinking: vec![mistral::ThinkingPart::Text { - text: text.clone(), - }], - }); - } + message_content + .push_part(mistral::MessagePart::Text { text: text.clone() }); } MessageContent::RedactedThinking(_) => {} MessageContent::ToolUse(_) => { @@ -447,28 +437,12 @@ pub fn into_mistral( Role::Assistant => { for content in &message.content { match content { - MessageContent::Text(text) => { + MessageContent::Text(text) | MessageContent::Thinking { text, .. } => { messages.push(mistral::RequestMessage::Assistant { - content: Some(mistral::MessageContent::Plain { - content: text.clone(), - }), + content: Some(text.clone()), tool_calls: Vec::new(), }); } - MessageContent::Thinking { text, .. } => { - if model.supports_thinking() { - messages.push(mistral::RequestMessage::Assistant { - content: Some(mistral::MessageContent::Multipart { - content: vec![mistral::MessagePart::Thinking { - thinking: vec![mistral::ThinkingPart::Text { - text: text.clone(), - }], - }], - }), - tool_calls: Vec::new(), - }); - } - } MessageContent::RedactedThinking(_) => {} MessageContent::Image(_) => {} MessageContent::ToolUse(tool_use) => { @@ -503,26 +477,11 @@ pub fn into_mistral( Role::System => { for content in &message.content { match content { - MessageContent::Text(text) => { + MessageContent::Text(text) | MessageContent::Thinking { text, .. } => { messages.push(mistral::RequestMessage::System { - content: mistral::MessageContent::Plain { - content: text.clone(), - }, + content: text.clone(), }); } - MessageContent::Thinking { text, .. } => { - if model.supports_thinking() { - messages.push(mistral::RequestMessage::System { - content: mistral::MessageContent::Multipart { - content: vec![mistral::MessagePart::Thinking { - thinking: vec![mistral::ThinkingPart::Text { - text: text.clone(), - }], - }], - }, - }); - } - } MessageContent::RedactedThinking(_) => {} MessageContent::Image(_) | MessageContent::ToolUse(_) @@ -535,8 +494,37 @@ pub fn into_mistral( } } + // The Mistral API requires that tool messages be followed by assistant messages, + // not user messages. When we have a tool->user sequence in the conversation, + // we need to insert a placeholder assistant message to maintain proper conversation + // flow and prevent API errors. This is a Mistral-specific requirement that differs + // from other language model APIs. + let messages = { + let mut fixed_messages = Vec::with_capacity(messages.len()); + let mut messages_iter = messages.into_iter().peekable(); + + while let Some(message) = messages_iter.next() { + let is_tool_message = matches!(message, mistral::RequestMessage::Tool { .. }); + fixed_messages.push(message); + + // Insert assistant message between tool and user messages + if is_tool_message { + if let Some(next_msg) = messages_iter.peek() { + if matches!(next_msg, mistral::RequestMessage::User { .. }) { + fixed_messages.push(mistral::RequestMessage::Assistant { + content: Some(" ".to_string()), + tool_calls: Vec::new(), + }); + } + } + } + } + + fixed_messages + }; + mistral::Request { - model: model.id().to_string(), + model, messages, stream, max_tokens: max_output_tokens, @@ -607,38 +595,8 @@ impl MistralEventMapper { }; let mut events = Vec::new(); - if let Some(content) = choice.delta.content.as_ref() { - match content { - mistral::MessageContentDelta::Text(text) => { - events.push(Ok(LanguageModelCompletionEvent::Text(text.clone()))); - } - mistral::MessageContentDelta::Parts(parts) => { - for part in parts { - match part { - mistral::MessagePart::Text { text } => { - events.push(Ok(LanguageModelCompletionEvent::Text(text.clone()))); - } - mistral::MessagePart::Thinking { thinking } => { - for tp in thinking.iter().cloned() { - match tp { - mistral::ThinkingPart::Text { text } => { - events.push(Ok( - LanguageModelCompletionEvent::Thinking { - text, - signature: None, - }, - )); - } - } - } - } - mistral::MessagePart::ImageUrl { .. } => { - // We currently don't emit a separate event for images in responses. - } - } - } - } - } + if let Some(content) = choice.delta.content.clone() { + events.push(Ok(LanguageModelCompletionEvent::Text(content))); } if let Some(tool_calls) = choice.delta.tool_calls.as_ref() { @@ -849,7 +807,7 @@ 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 Mistral, you need to add an API key. Follow these steps:")) + .child(Label::new("To use Zed's assistant with Mistral, you need to add an API key. Follow these steps:")) .child( List::new() .child(InstructionListItem::new( @@ -950,7 +908,7 @@ mod tests { thinking_allowed: true, }; - let mistral_request = into_mistral(request, mistral::Model::MistralSmallLatest, None); + let mistral_request = into_mistral(request, "mistral-small-latest".into(), None); assert_eq!(mistral_request.model, "mistral-small-latest"); assert_eq!(mistral_request.temperature, Some(0.5)); @@ -983,7 +941,7 @@ mod tests { thinking_allowed: true, }; - let mistral_request = into_mistral(request, mistral::Model::Pixtral12BLatest, None); + let mistral_request = into_mistral(request, "pixtral-12b-latest".into(), None); assert_eq!(mistral_request.messages.len(), 1); assert!(matches!( diff --git a/crates/language_models/src/provider/ollama.rs b/crates/language_models/src/provider/ollama.rs index 3f2d47fba3..c20ea0ee1e 100644 --- a/crates/language_models/src/provider/ollama.rs +++ b/crates/language_models/src/provider/ollama.rs @@ -237,7 +237,7 @@ impl LanguageModelProvider for OllamaLanguageModelProvider { .map(|model| { Arc::new(OllamaLanguageModel { id: LanguageModelId::from(model.name.clone()), - model, + model: model.clone(), http_client: self.http_client.clone(), request_limiter: RateLimiter::new(4), }) as Arc @@ -255,12 +255,7 @@ impl LanguageModelProvider for OllamaLanguageModelProvider { self.state.update(cx, |state, cx| state.authenticate(cx)) } - fn configuration_view( - &self, - _target_agent: language_model::ConfigurationViewTargetAgent, - window: &mut Window, - cx: &mut App, - ) -> AnyView { + fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView { let state = self.state.clone(); cx.new(|cx| ConfigurationView::new(state, window, cx)) .into() @@ -613,7 +608,7 @@ impl Render for ConfigurationView { Button::new("ollama-site", "Ollama") .style(ButtonStyle::Subtle) .icon(IconName::ArrowUpRight) - .icon_size(IconSize::Small) + .icon_size(IconSize::XSmall) .icon_color(Color::Muted) .on_click(move |_, _, cx| cx.open_url(OLLAMA_SITE)) .into_any_element(), @@ -626,7 +621,7 @@ impl Render for ConfigurationView { ) .style(ButtonStyle::Subtle) .icon(IconName::ArrowUpRight) - .icon_size(IconSize::Small) + .icon_size(IconSize::XSmall) .icon_color(Color::Muted) .on_click(move |_, _, cx| { cx.open_url(OLLAMA_DOWNLOAD_URL) @@ -636,10 +631,10 @@ impl Render for ConfigurationView { } }) .child( - Button::new("view-models", "View All Models") + Button::new("view-models", "All Models") .style(ButtonStyle::Subtle) .icon(IconName::ArrowUpRight) - .icon_size(IconSize::Small) + .icon_size(IconSize::XSmall) .icon_color(Color::Muted) .on_click(move |_, _, cx| cx.open_url(OLLAMA_LIBRARY_URL)), ), @@ -663,7 +658,7 @@ impl Render for ConfigurationView { Button::new("retry_ollama_models", "Connect") .icon_position(IconPosition::Start) .icon_size(IconSize::XSmall) - .icon(IconName::PlayFilled) + .icon(IconName::Play) .on_click(cx.listener(move |this, _, _, cx| { this.retry_connection(cx) })), diff --git a/crates/language_models/src/provider/open_ai.rs b/crates/language_models/src/provider/open_ai.rs index 4348fd4211..6c4d4c9b3e 100644 --- a/crates/language_models/src/provider/open_ai.rs +++ b/crates/language_models/src/provider/open_ai.rs @@ -14,7 +14,7 @@ use language_model::{ RateLimiter, Role, StopReason, TokenUsage, }; use menu; -use open_ai::{ImageUrl, Model, ReasoningEffort, ResponseStreamEvent, stream_completion}; +use open_ai::{ImageUrl, Model, ResponseStreamEvent, stream_completion}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsStore}; @@ -45,7 +45,6 @@ pub struct AvailableModel { pub max_tokens: u64, pub max_output_tokens: Option, pub max_completion_tokens: Option, - pub reasoning_effort: Option, } pub struct OpenAiLanguageModelProvider { @@ -75,7 +74,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 +93,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| { @@ -119,7 +118,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)?; ( @@ -214,7 +213,6 @@ impl LanguageModelProvider for OpenAiLanguageModelProvider { max_tokens: model.max_tokens, max_output_tokens: model.max_output_tokens, max_completion_tokens: model.max_completion_tokens, - reasoning_effort: model.reasoning_effort.clone(), }, ); } @@ -233,12 +231,7 @@ impl LanguageModelProvider for OpenAiLanguageModelProvider { self.state.update(cx, |state, cx| state.authenticate(cx)) } - fn configuration_view( - &self, - _target_agent: language_model::ConfigurationViewTargetAgent, - window: &mut Window, - cx: &mut App, - ) -> AnyView { + fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView { cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx)) .into() } @@ -308,25 +301,7 @@ impl LanguageModel for OpenAiLanguageModel { } fn supports_images(&self) -> bool { - use open_ai::Model; - match &self.model { - Model::FourOmni - | Model::FourOmniMini - | Model::FourPointOne - | Model::FourPointOneMini - | Model::FourPointOneNano - | Model::Five - | Model::FiveMini - | Model::FiveNano - | Model::O1 - | Model::O3 - | Model::O4Mini => true, - Model::ThreePointFiveTurbo - | Model::Four - | Model::FourTurbo - | Model::O3Mini - | Model::Custom { .. } => false, - } + false } fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool { @@ -375,9 +350,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(), ); let completions = self.stream_completion(request, cx); async move { @@ -392,9 +365,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, - reasoning_effort: Option, ) -> open_ai::Request { let stream = !model_id.starts_with("o1-"); @@ -404,7 +375,7 @@ pub fn into_open_ai( match content { MessageContent::Text(text) | MessageContent::Thinking { text, .. } => { add_message_content_part( - open_ai::MessagePart::Text { text }, + open_ai::MessagePart::Text { text: text }, message.role, &mut messages, ) @@ -484,11 +455,6 @@ pub fn into_open_ai( } else { None }, - prompt_cache_key: if supports_prompt_cache_key { - request.thread_id - } else { - None - }, tools: request .tools .into_iter() @@ -505,7 +471,6 @@ pub fn into_open_ai( LanguageModelToolChoice::Any => open_ai::ToolChoice::Required, LanguageModelToolChoice::None => open_ai::ToolChoice::None, }), - reasoning_effort, } } @@ -709,10 +674,6 @@ pub fn count_open_ai_tokens( | Model::O3 | Model::O3Mini | Model::O4Mini => tiktoken_rs::num_tokens_from_messages(model.id(), &messages), - // GPT-5 models don't have tiktoken support yet; fall back on gpt-4o tokenizer - Model::Five | Model::FiveMini | Model::FiveNano => { - tiktoken_rs::num_tokens_from_messages("gpt-4o", &messages) - } } .map(|tokens| tokens as u64) }) @@ -819,7 +780,7 @@ impl Render for ConfigurationView { let api_key_section = if self.should_render_editor(cx) { v_flex() .on_action(cx.listener(Self::save_api_key)) - .child(Label::new("To use Zed's agent with OpenAI, you need to add an API key. Follow these steps:")) + .child(Label::new("To use Zed's assistant with OpenAI, you need to add an API key. Follow these steps:")) .child( List::new() .child(InstructionListItem::new( @@ -904,10 +865,10 @@ impl Render for ConfigurationView { .child( Button::new("docs", "Learn More") .icon(IconName::ArrowUpRight) - .icon_size(IconSize::Small) + .icon_size(IconSize::XSmall) .icon_color(Color::Muted) .on_click(move |_, _window, cx| { - cx.open_url("https://zed.dev/docs/ai/llm-providers#openai-api-compatible") + cx.open_url("https://zed.dev/docs/ai/configuration#openai-api-compatible") }), ); diff --git a/crates/language_models/src/provider/open_ai_compatible.rs b/crates/language_models/src/provider/open_ai_compatible.rs index 55df534cc9..64add5483d 100644 --- a/crates/language_models/src/provider/open_ai_compatible.rs +++ b/crates/language_models/src/provider/open_ai_compatible.rs @@ -38,27 +38,6 @@ pub struct AvailableModel { pub max_tokens: u64, pub max_output_tokens: Option, pub max_completion_tokens: Option, - #[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 { @@ -87,7 +66,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| { @@ -103,7 +82,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| { @@ -126,7 +105,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,12 +222,7 @@ impl LanguageModelProvider for OpenAiCompatibleLanguageModelProvider { self.state.update(cx, |state, cx| state.authenticate(cx)) } - fn configuration_view( - &self, - _target_agent: language_model::ConfigurationViewTargetAgent, - window: &mut Window, - cx: &mut App, - ) -> AnyView { + fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView { cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx)) .into() } @@ -319,17 +293,17 @@ impl LanguageModel for OpenAiCompatibleLanguageModel { } fn supports_tools(&self) -> bool { - self.model.capabilities.tools + true } fn supports_images(&self) -> bool { - self.model.capabilities.images + false } fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool { match choice { - LanguageModelToolChoice::Auto => self.model.capabilities.tools, - LanguageModelToolChoice::Any => self.model.capabilities.tools, + LanguageModelToolChoice::Auto => true, + LanguageModelToolChoice::Any => true, LanguageModelToolChoice::None => true, } } @@ -381,14 +355,7 @@ impl LanguageModel for OpenAiCompatibleLanguageModel { LanguageModelCompletionError, >, > { - let request = into_open_ai( - request, - &self.model.name, - self.model.capabilities.parallel_tool_calls, - self.model.capabilities.prompt_cache_key, - self.max_output_tokens(), - None, - ); + let request = into_open_ai(request, &self.model.name, true, self.max_output_tokens()); let completions = self.stream_completion(request, cx); async move { let mapper = OpenAiEventMapper::new(); @@ -499,7 +466,7 @@ impl Render for ConfigurationView { let api_key_section = if self.should_render_editor(cx) { v_flex() .on_action(cx.listener(Self::save_api_key)) - .child(Label::new("To use Zed's agent with an OpenAI-compatible provider, you need to add an API key.")) + .child(Label::new("To use Zed's assistant with an OpenAI compatible provider, you need to add an API key.")) .child( div() .pt(DynamicSpacing::Base04.rems(cx)) diff --git a/crates/language_models/src/provider/open_router.rs b/crates/language_models/src/provider/open_router.rs index 8f2abfce35..5a6acc4329 100644 --- a/crates/language_models/src/provider/open_router.rs +++ b/crates/language_models/src/provider/open_router.rs @@ -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| { @@ -157,7 +157,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)?; ( @@ -306,12 +306,7 @@ impl LanguageModelProvider for OpenRouterLanguageModelProvider { self.state.update(cx, |state, cx| state.authenticate(cx)) } - fn configuration_view( - &self, - _target_agent: language_model::ConfigurationViewTargetAgent, - window: &mut Window, - cx: &mut App, - ) -> AnyView { + fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView { cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx)) .into() } @@ -860,7 +855,7 @@ 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 OpenRouter, you need to add an API key. Follow these steps:")) + .child(Label::new("To use Zed's assistant with OpenRouter, you need to add an API key. Follow these steps:")) .child( List::new() .child(InstructionListItem::new( diff --git a/crates/language_models/src/provider/vercel.rs b/crates/language_models/src/provider/vercel.rs index 84f3175d1e..037ce467d0 100644 --- a/crates/language_models/src/provider/vercel.rs +++ b/crates/language_models/src/provider/vercel.rs @@ -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,12 +230,7 @@ impl LanguageModelProvider for VercelLanguageModelProvider { self.state.update(cx, |state, cx| state.authenticate(cx)) } - fn configuration_view( - &self, - _target_agent: language_model::ConfigurationViewTargetAgent, - window: &mut Window, - cx: &mut App, - ) -> AnyView { + fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView { cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx)) .into() } @@ -360,9 +355,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, ); let completions = self.stream_completion(request, cx); async move { diff --git a/crates/language_models/src/provider/x_ai.rs b/crates/language_models/src/provider/x_ai.rs index b37a55e19f..5f6034571b 100644 --- a/crates/language_models/src/provider/x_ai.rs +++ b/crates/language_models/src/provider/x_ai.rs @@ -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,12 +230,7 @@ impl LanguageModelProvider for XAiLanguageModelProvider { self.state.update(cx, |state, cx| state.authenticate(cx)) } - fn configuration_view( - &self, - _target_agent: language_model::ConfigurationViewTargetAgent, - window: &mut Window, - cx: &mut App, - ) -> AnyView { + fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView { cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx)) .into() } @@ -364,9 +359,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, ); let completions = self.stream_completion(request, cx); async move { diff --git a/crates/language_models/src/ui/instruction_list_item.rs b/crates/language_models/src/ui/instruction_list_item.rs index bdb5fbe242..794a85b400 100644 --- a/crates/language_models/src/ui/instruction_list_item.rs +++ b/crates/language_models/src/ui/instruction_list_item.rs @@ -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; + let link = button_link.clone(); let unique_id = SharedString::from(format!("{}-button", self.label)); h_flex() @@ -47,7 +47,7 @@ impl IntoElement for InstructionListItem { Button::new(unique_id, button_label) .style(ButtonStyle::Subtle) .icon(IconName::ArrowUpRight) - .icon_size(IconSize::Small) + .icon_size(IconSize::XSmall) .icon_color(Color::Muted) .on_click(move |_, _window, cx| cx.open_url(&link)), ) diff --git a/crates/language_selector/src/active_buffer_language.rs b/crates/language_selector/src/active_buffer_language.rs index 56924c4cd2..250d0c23d8 100644 --- a/crates/language_selector/src/active_buffer_language.rs +++ b/crates/language_selector/src/active_buffer_language.rs @@ -1,9 +1,8 @@ -use editor::{Editor, EditorSettings}; +use editor::Editor; use gpui::{ Context, Entity, IntoElement, ParentElement, Render, Subscription, WeakEntity, Window, div, }; use language::LanguageName; -use settings::Settings as _; use ui::{Button, ButtonCommon, Clickable, FluentBuilder, LabelSize, Tooltip}; use workspace::{StatusItemView, Workspace, item::ItemHandle}; @@ -28,10 +27,10 @@ impl ActiveBufferLanguage { self.active_language = Some(None); let editor = editor.read(cx); - if let Some((_, buffer, _)) = editor.active_excerpt(cx) - && let Some(language) = buffer.read(cx).language() - { - self.active_language = Some(Some(language.name())); + if let Some((_, buffer, _)) = editor.active_excerpt(cx) { + if let Some(language) = buffer.read(cx).language() { + self.active_language = Some(Some(language.name())); + } } cx.notify(); @@ -40,13 +39,6 @@ impl ActiveBufferLanguage { impl Render for ActiveBufferLanguage { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - if !EditorSettings::get_global(cx) - .status_bar - .active_language_button - { - return div(); - } - div().when_some(self.active_language.as_ref(), |el, active_language| { let active_language_text = if let Some(active_language_text) = active_language { active_language_text.to_string() diff --git a/crates/language_selector/src/language_selector.rs b/crates/language_selector/src/language_selector.rs index f6e2d75015..4c03430553 100644 --- a/crates/language_selector/src/language_selector.rs +++ b/crates/language_selector/src/language_selector.rs @@ -86,10 +86,7 @@ impl LanguageSelector { impl Render for LanguageSelector { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - v_flex() - .key_context("LanguageSelector") - .w(rems(34.)) - .child(self.picker.clone()) + v_flex().w(rems(34.)).child(self.picker.clone()) } } @@ -124,13 +121,13 @@ impl LanguageSelectorDelegate { .into_iter() .filter_map(|name| { language_registry - .available_language_for_name(name.as_ref())? + .available_language_for_name(&name)? .hidden() .not() .then_some(name) }) .enumerate() - .map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name.as_ref())) + .map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, &name)) .collect::>(); Self { diff --git a/crates/language_tools/src/key_context_view.rs b/crates/language_tools/src/key_context_view.rs index 4140713544..88131781ec 100644 --- a/crates/language_tools/src/key_context_view.rs +++ b/crates/language_tools/src/key_context_view.rs @@ -4,6 +4,7 @@ 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, @@ -70,10 +71,12 @@ impl KeyContextView { } else { None } - } else if this.action_matches(&e.action, binding.action()) { - Some(true) } else { - Some(false) + if this.action_matches(&e.action, binding.action()) { + Some(true) + } else { + Some(false) + } }; let predicate = if let Some(predicate) = binding.predicate() { format!("{}", predicate) @@ -95,7 +98,9 @@ impl KeyContextView { cx.notify(); }); let sub2 = cx.observe_pending_input(window, |this, window, cx| { - this.pending_keystrokes = window.pending_input_keystrokes().map(|k| k.to_vec()); + this.pending_keystrokes = window + .pending_input_keystrokes() + .map(|k| k.iter().cloned().collect()); if this.pending_keystrokes.is_some() { this.last_keystrokes.take(); } @@ -168,8 +173,7 @@ impl Item for KeyContextView { impl Render for KeyContextView { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl ui::IntoElement { use itertools::Itertools; - - let key_equivalents = cx.keyboard_mapper().get_key_equivalents(); + let key_equivalents = get_key_equivalents(cx.keyboard_layout().id()); v_flex() .id("key-context-view") .overflow_scroll() diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index d5206c1f26..2b0e13f4be 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -253,36 +253,36 @@ impl LogStore { let copilot_subscription = 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 - && 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::( - 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, |this, copilot, inline_completion_event, cx| { + if let copilot::Event::CopilotLanguageServerStarted = inline_completion_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::( + 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, + ); + } } }) }); @@ -406,7 +406,10 @@ impl LogStore { server_state.worktree_id = Some(worktree_id); } - if let Some(server) = server.filter(|_| server_state.io_logs_subscription.is_none()) { + 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| { @@ -658,7 +661,7 @@ impl LogStore { IoKind::StdOut => true, IoKind::StdIn => false, IoKind::StdErr => { - self.add_language_server_log(language_server_id, MessageType::LOG, message, cx); + self.add_language_server_log(language_server_id, MessageType::LOG, &message, cx); return Some(()); } }; @@ -730,14 +733,16 @@ 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) - && 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) { + 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), + } } } } else if let Some(server_id) = first_server_id_for_project { @@ -771,17 +776,21 @@ impl LspLogView { ], cx, ); - if text.len() > 1024 - && let Some((fold_offset, _)) = + if text.len() > 1024 { + if let Some((fold_offset, _)) = text.char_indices().dropping(1024).next() - && fold_offset < text.len() - { - editor.fold_ranges( - vec![last_offset + fold_offset..last_offset + text.len()], - false, - window, - cx, - ); + { + if 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 { @@ -927,7 +936,7 @@ impl LspLogView { let state = log_store.language_servers.get(&server_id)?; Some(LogMenuItem { server_id, - server_name: name, + server_name: name.clone(), server_kind: state.kind.clone(), worktree_root_name: "supplementary".to_string(), rpc_trace_enabled: state.rpc_state.is_some(), @@ -1302,14 +1311,14 @@ impl ToolbarItemView for LspLogToolbarItemView { _: &mut Window, cx: &mut Context, ) -> workspace::ToolbarItemLocation { - if let Some(item) = active_pane_item - && let Some(log_view) = item.downcast::() - { - self.log_view = Some(log_view.clone()); - self._log_view_subscription = Some(cx.observe(&log_view, |_, _, cx| { - cx.notify(); - })); - return ToolbarItemLocation::PrimaryLeft; + if let Some(item) = active_pane_item { + if let Some(log_view) = item.downcast::() { + self.log_view = Some(log_view.clone()); + self._log_view_subscription = Some(cx.observe(&log_view, |_, _, cx| { + cx.notify(); + })); + return ToolbarItemLocation::PrimaryLeft; + } } self.log_view = None; self._log_view_subscription = None; @@ -1349,7 +1358,7 @@ impl Render for LspLogToolbarItemView { }) .collect(); - let log_toolbar_view = cx.entity(); + let log_toolbar_view = cx.entity().clone(); let lsp_menu = PopoverMenu::new("LspLogView") .anchor(Corner::TopLeft) @@ -1524,7 +1533,7 @@ impl Render for LspLogToolbarItemView { .icon_color(Color::Muted), ) .menu({ - let log_view = log_view; + let log_view = log_view.clone(); move |window, cx| { let id = log_view.read(cx).current_server_id?; @@ -1592,7 +1601,7 @@ impl Render for LspLogToolbarItemView { .icon_color(Color::Muted), ) .menu({ - let log_view = log_view; + let log_view = log_view.clone(); move |window, cx| { let id = log_view.read(cx).current_server_id?; @@ -1743,5 +1752,6 @@ pub enum Event { } impl EventEmitter for LogStore {} +impl EventEmitter for LspLogView {} impl EventEmitter for LspLogView {} impl EventEmitter for LspLogView {} diff --git a/crates/language_tools/src/lsp_tool.rs b/crates/language_tools/src/lsp_tool.rs index dd3e80212f..a339f3b941 100644 --- a/crates/language_tools/src/lsp_tool.rs +++ b/crates/language_tools/src/lsp_tool.rs @@ -349,6 +349,7 @@ impl LanguageServerState { .detach(); } else { cx.propagate(); + return; } } }, @@ -522,6 +523,7 @@ impl LspTool { 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; } } else if lsp_tool.lsp_menu.take().is_some() { cx.notify(); @@ -1005,7 +1007,7 @@ impl Render for LspTool { (None, "All Servers Operational") }; - let lsp_tool = cx.entity(); + let lsp_tool = cx.entity().clone(); div().child( PopoverMenu::new("lsp-tool") @@ -1013,7 +1015,7 @@ impl Render for LspTool { .anchor(Corner::BottomLeft) .with_handle(self.popover_menu_handle.clone()) .trigger_with_tooltip( - IconButton::new("zed-lsp-tool-button", IconName::BoltOutlined) + IconButton::new("zed-lsp-tool-button", IconName::Bolt) .when_some(indicator, IconButton::indicator) .icon_size(IconSize::Small) .indicator_border_color(Some(cx.theme().colors().status_bar_background)), diff --git a/crates/language_tools/src/syntax_tree_view.rs b/crates/language_tools/src/syntax_tree_view.rs index cf84ac34c4..eadba2c1d2 100644 --- a/crates/language_tools/src/syntax_tree_view.rs +++ b/crates/language_tools/src/syntax_tree_view.rs @@ -103,11 +103,12 @@ impl SyntaxTreeView { window: &mut Window, cx: &mut Context, ) { - if let Some(item) = active_item - && item.item_id() != cx.entity_id() - && let Some(editor) = item.act_as::(cx) - { - self.set_editor(editor, window, cx); + if let Some(item) = active_item { + if item.item_id() != cx.entity_id() { + if let Some(editor) = item.act_as::(cx) { + self.set_editor(editor, window, cx); + } + } } } @@ -156,7 +157,7 @@ impl SyntaxTreeView { .buffer_snapshot .range_to_buffer_ranges(selection_range) .pop()?; - let buffer = multi_buffer.buffer(buffer.remote_id()).unwrap(); + let buffer = multi_buffer.buffer(buffer.remote_id()).unwrap().clone(); Some((buffer, range, excerpt_id)) })?; @@ -455,7 +456,7 @@ impl SyntaxTreeToolbarItemView { let active_layer = buffer_state.active_layer.clone()?; let active_buffer = buffer_state.buffer.read(cx).snapshot(); - let view = cx.entity(); + let view = cx.entity().clone(); Some( PopoverMenu::new("Syntax Tree") .trigger(Self::render_header(&active_layer)) @@ -536,12 +537,12 @@ impl ToolbarItemView for SyntaxTreeToolbarItemView { window: &mut Window, cx: &mut Context, ) -> ToolbarItemLocation { - if let Some(item) = active_pane_item - && let Some(view) = item.downcast::() - { - 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 { + if let Some(view) = item.downcast::() { + 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; diff --git a/crates/languages/Cargo.toml b/crates/languages/Cargo.toml index 8e25818070..260126da63 100644 --- a/crates/languages/Cargo.toml +++ b/crates/languages/Cargo.toml @@ -36,7 +36,6 @@ load-grammars = [ [dependencies] anyhow.workspace = true async-compression.workspace = true -async-fs.workspace = true async-tar.workspace = true async-trait.workspace = true chrono.workspace = true @@ -63,7 +62,6 @@ regex.workspace = true rope.workspace = true rust-embed.workspace = true schemars.workspace = true -sha2.workspace = true serde.workspace = true serde_json.workspace = true serde_json_lenient.workspace = true @@ -71,7 +69,6 @@ settings.workspace = true smol.workspace = true snippet_provider.workspace = true task.workspace = true -tempfile.workspace = true toml.workspace = true tree-sitter = { workspace = true, optional = true } tree-sitter-bash = { workspace = true, optional = true } diff --git a/crates/languages/src/bash/overrides.scm b/crates/languages/src/bash/overrides.scm deleted file mode 100644 index 81fec9a5f5..0000000000 --- a/crates/languages/src/bash/overrides.scm +++ /dev/null @@ -1,2 +0,0 @@ -(comment) @comment.inclusive -(string) @string diff --git a/crates/languages/src/c.rs b/crates/languages/src/c.rs index 2820f55a49..c06c35ee69 100644 --- a/crates/languages/src/c.rs +++ b/crates/languages/src/c.rs @@ -2,16 +2,14 @@ use anyhow::{Context as _, Result, bail}; use async_trait::async_trait; use futures::StreamExt; use gpui::{App, AsyncApp}; -use http_client::github::{AssetKind, GitHubLspBinaryVersion, latest_github_release}; +use http_client::github::{GitHubLspBinaryVersion, latest_github_release}; pub use language::*; use lsp::{InitializeParams, LanguageServerBinary, LanguageServerName}; use project::lsp_store::clangd_ext; use serde_json::json; use smol::fs; use std::{any::Any, env::consts, path::PathBuf, sync::Arc}; -use util::{ResultExt, fs::remove_matching, maybe, merge_json_value_into}; - -use crate::github_download::{GithubBinaryMetadata, download_server_binary}; +use util::{ResultExt, archive::extract_zip, fs::remove_matching, maybe, merge_json_value_into}; pub struct CLspAdapter; @@ -22,13 +20,13 @@ impl CLspAdapter { #[async_trait(?Send)] impl super::LspAdapter for CLspAdapter { fn name(&self) -> LanguageServerName { - Self::SERVER_NAME + Self::SERVER_NAME.clone() } async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, - _: Option, + _: Arc, _: &AsyncApp, ) -> Option { let path = delegate.which(Self::SERVER_NAME.as_ref()).await?; @@ -60,7 +58,6 @@ impl super::LspAdapter for CLspAdapter { let version = GitHubLspBinaryVersion { name: release.tag_name, url: asset.browser_download_url.clone(), - digest: asset.digest.clone(), }; Ok(Box::new(version) as Box<_>) } @@ -71,72 +68,32 @@ impl super::LspAdapter for CLspAdapter { container_dir: PathBuf, delegate: &dyn LspAdapterDelegate, ) -> Result { - let GitHubLspBinaryVersion { - name, - url, - digest: expected_digest, - } = *version.downcast::().unwrap(); - let version_dir = container_dir.join(format!("clangd_{name}")); + let version = version.downcast::().unwrap(); + let version_dir = container_dir.join(format!("clangd_{}", version.name)); let binary_path = version_dir.join("bin/clangd"); - let binary = LanguageServerBinary { - path: binary_path.clone(), - env: None, - arguments: Default::default(), - }; - - let metadata_path = version_dir.join("metadata"); - let metadata = GithubBinaryMetadata::read_from_file(&metadata_path) - .await - .ok(); - if let Some(metadata) = metadata { - let validity_check = async || { - delegate - .try_exec(LanguageServerBinary { - path: binary_path.clone(), - arguments: vec!["--version".into()], - env: None, - }) - .await - .inspect_err(|err| { - log::warn!("Unable to run {binary_path:?} asset, redownloading: {err}",) - }) - }; - if let (Some(actual_digest), Some(expected_digest)) = - (&metadata.digest, &expected_digest) - { - if actual_digest == expected_digest { - if validity_check().await.is_ok() { - return Ok(binary); - } - } else { - log::info!( - "SHA-256 mismatch for {binary_path:?} asset, downloading new asset. Expected: {expected_digest}, Got: {actual_digest}" - ); - } - } else if validity_check().await.is_ok() { - return Ok(binary); - } + if fs::metadata(&binary_path).await.is_err() { + let mut response = delegate + .http_client() + .get(&version.url, Default::default(), true) + .await + .context("error downloading release")?; + anyhow::ensure!( + response.status().is_success(), + "download failed with status {}", + response.status().to_string() + ); + extract_zip(&container_dir, response.body_mut()) + .await + .with_context(|| format!("unzipping clangd archive to {container_dir:?}"))?; + remove_matching(&container_dir, |entry| entry != version_dir).await; } - download_server_binary( - delegate, - &url, - expected_digest.as_deref(), - &container_dir, - AssetKind::Zip, - ) - .await?; - remove_matching(&container_dir, |entry| entry != version_dir).await; - GithubBinaryMetadata::write_to_file( - &GithubBinaryMetadata { - metadata_version: 1, - digest: expected_digest, - }, - &metadata_path, - ) - .await?; - Ok(binary) + Ok(LanguageServerBinary { + path: binary_path, + env: None, + arguments: Vec::new(), + }) } async fn cached_server_binary( @@ -253,7 +210,8 @@ impl super::LspAdapter for CLspAdapter { .grammar() .and_then(|g| g.highlight_id_for_name(highlight_name?)) { - let mut label = CodeLabel::plain(label, completion.filter_text.as_deref()); + let mut label = + CodeLabel::plain(label.to_string(), completion.filter_text.as_deref()); label.runs.push(( 0..label.text.rfind('(').unwrap_or(label.text.len()), highlight_id, @@ -263,7 +221,10 @@ impl super::LspAdapter for CLspAdapter { } _ => {} } - Some(CodeLabel::plain(label, completion.filter_text.as_deref())) + Some(CodeLabel::plain( + label.to_string(), + completion.filter_text.as_deref(), + )) } async fn label_for_symbol( diff --git a/crates/languages/src/cpp/config.toml b/crates/languages/src/cpp/config.toml index 7e24415f9d..fab88266d7 100644 --- a/crates/languages/src/cpp/config.toml +++ b/crates/languages/src/cpp/config.toml @@ -1,6 +1,6 @@ name = "C++" grammar = "cpp" -path_suffixes = ["cc", "hh", "cpp", "h", "hpp", "cxx", "hxx", "c++", "ipp", "inl", "ino", "ixx", "cu", "cuh", "C", "H"] +path_suffixes = ["cc", "hh", "cpp", "h", "hpp", "cxx", "hxx", "c++", "ipp", "inl", "ixx", "cu", "cuh", "C", "H"] line_comments = ["// ", "/// ", "//! "] decrease_indent_patterns = [ { pattern = "^\\s*\\{.*\\}?\\s*$", valid_after = ["if", "for", "while", "do", "switch", "else"] }, diff --git a/crates/languages/src/cpp/outline.scm b/crates/languages/src/cpp/outline.scm index c897366558..448fe35220 100644 --- a/crates/languages/src/cpp/outline.scm +++ b/crates/languages/src/cpp/outline.scm @@ -149,9 +149,7 @@ parameters: (parameter_list "(" @context ")" @context))) - ; Fields declarations may define multiple fields, and so @item is on the - ; declarator so they each get distinct ranges. - ] @item - (type_qualifier)? @context) + ] + (type_qualifier)? @context) @item (comment) @annotation diff --git a/crates/languages/src/css.rs b/crates/languages/src/css.rs index 2480d40268..f2a94809a0 100644 --- a/crates/languages/src/css.rs +++ b/crates/languages/src/css.rs @@ -2,10 +2,10 @@ use anyhow::{Context as _, Result}; use async_trait::async_trait; use futures::StreamExt; use gpui::AsyncApp; -use language::{LspAdapter, LspAdapterDelegate, Toolchain}; +use language::{LanguageToolchainStore, LspAdapter, LspAdapterDelegate}; use lsp::{LanguageServerBinary, LanguageServerName}; -use node_runtime::{NodeRuntime, VersionStrategy}; -use project::{Fs, lsp_store::language_server_settings}; +use node_runtime::NodeRuntime; +use project::Fs; use serde_json::json; use smol::fs; use std::{ @@ -14,7 +14,7 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; -use util::{ResultExt, maybe, merge_json_value_into}; +use util::{ResultExt, maybe}; const SERVER_PATH: &str = "node_modules/vscode-langservers-extracted/bin/vscode-css-language-server"; @@ -43,7 +43,7 @@ impl LspAdapter for CssLspAdapter { async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, - _: Option, + _: Arc, _: &AsyncApp, ) -> Option { let path = delegate @@ -103,12 +103,7 @@ impl LspAdapter for CssLspAdapter { let should_install_language_server = self .node - .should_install_npm_package( - Self::PACKAGE_NAME, - &server_path, - container_dir, - VersionStrategy::Latest(version), - ) + .should_install_npm_package(Self::PACKAGE_NAME, &server_path, &container_dir, &version) .await; if should_install_language_server { @@ -139,37 +134,6 @@ impl LspAdapter for CssLspAdapter { "provideFormatter": true }))) } - - async fn workspace_configuration( - self: Arc, - _: &dyn Fs, - delegate: &Arc, - _: Option, - cx: &mut AsyncApp, - ) -> Result { - let mut default_config = json!({ - "css": { - "lint": {} - }, - "less": { - "lint": {} - }, - "scss": { - "lint": {} - } - }); - - let project_options = cx.update(|cx| { - language_server_settings(delegate.as_ref(), &self.name(), cx) - .and_then(|s| s.settings.clone()) - })?; - - if let Some(override_options) = project_options { - merge_json_value_into(override_options, &mut default_config); - } - - Ok(default_config) - } } async fn get_cached_server_binary( diff --git a/crates/languages/src/github_download.rs b/crates/languages/src/github_download.rs deleted file mode 100644 index 766c894fbb..0000000000 --- a/crates/languages/src/github_download.rs +++ /dev/null @@ -1,190 +0,0 @@ -use std::{path::Path, pin::Pin, task::Poll}; - -use anyhow::{Context, Result}; -use async_compression::futures::bufread::GzipDecoder; -use futures::{AsyncRead, AsyncSeek, AsyncSeekExt, AsyncWrite, io::BufReader}; -use http_client::github::AssetKind; -use language::LspAdapterDelegate; -use sha2::{Digest, Sha256}; - -#[derive(serde::Deserialize, serde::Serialize, Debug)] -pub(crate) struct GithubBinaryMetadata { - pub(crate) metadata_version: u64, - pub(crate) digest: Option, -} - -impl GithubBinaryMetadata { - pub(crate) async fn read_from_file(metadata_path: &Path) -> Result { - let metadata_content = async_fs::read_to_string(metadata_path) - .await - .with_context(|| format!("reading metadata file at {metadata_path:?}"))?; - serde_json::from_str(&metadata_content) - .with_context(|| format!("parsing metadata file at {metadata_path:?}")) - } - - pub(crate) async fn write_to_file(&self, metadata_path: &Path) -> Result<()> { - let metadata_content = serde_json::to_string(self) - .with_context(|| format!("serializing metadata for {metadata_path:?}"))?; - async_fs::write(metadata_path, metadata_content.as_bytes()) - .await - .with_context(|| format!("writing metadata file at {metadata_path:?}"))?; - Ok(()) - } -} - -pub(crate) async fn download_server_binary( - delegate: &dyn LspAdapterDelegate, - url: &str, - digest: Option<&str>, - destination_path: &Path, - asset_kind: AssetKind, -) -> Result<(), anyhow::Error> { - log::info!("downloading github artifact from {url}"); - let mut response = delegate - .http_client() - .get(url, Default::default(), true) - .await - .with_context(|| format!("downloading release from {url}"))?; - let body = response.body_mut(); - match digest { - Some(expected_sha_256) => { - let temp_asset_file = tempfile::NamedTempFile::new() - .with_context(|| format!("creating a temporary file for {url}"))?; - let (temp_asset_file, _temp_guard) = temp_asset_file.into_parts(); - let mut writer = HashingWriter { - writer: async_fs::File::from(temp_asset_file), - hasher: Sha256::new(), - }; - futures::io::copy(&mut BufReader::new(body), &mut writer) - .await - .with_context(|| { - format!("saving archive contents into the temporary file for {url}",) - })?; - let asset_sha_256 = format!("{:x}", writer.hasher.finalize()); - - anyhow::ensure!( - asset_sha_256 == expected_sha_256, - "{url} asset got SHA-256 mismatch. Expected: {expected_sha_256}, Got: {asset_sha_256}", - ); - writer - .writer - .seek(std::io::SeekFrom::Start(0)) - .await - .with_context(|| format!("seeking temporary file {destination_path:?}",))?; - stream_file_archive(&mut writer.writer, url, destination_path, asset_kind) - .await - .with_context(|| { - format!("extracting downloaded asset for {url} into {destination_path:?}",) - })?; - } - None => stream_response_archive(body, url, destination_path, asset_kind) - .await - .with_context(|| { - format!("extracting response for asset {url} into {destination_path:?}",) - })?, - } - Ok(()) -} - -async fn stream_response_archive( - response: impl AsyncRead + Unpin, - url: &str, - destination_path: &Path, - asset_kind: AssetKind, -) -> Result<()> { - match asset_kind { - 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?; - } - }; - Ok(()) -} - -async fn stream_file_archive( - file_archive: impl AsyncRead + AsyncSeek + Unpin, - url: &str, - destination_path: &Path, - asset_kind: AssetKind, -) -> Result<()> { - match asset_kind { - AssetKind::TarGz => extract_tar_gz(destination_path, url, file_archive).await?, - 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?; - } - #[cfg(windows)] - AssetKind::Zip => { - util::archive::extract_zip(destination_path, file_archive).await?; - } - }; - Ok(()) -} - -async fn extract_tar_gz( - destination_path: &Path, - url: &str, - from: impl AsyncRead + Unpin, -) -> Result<(), anyhow::Error> { - let decompressed_bytes = GzipDecoder::new(BufReader::new(from)); - let archive = async_tar::Archive::new(decompressed_bytes); - archive - .unpack(&destination_path) - .await - .with_context(|| format!("extracting {url} to {destination_path:?}"))?; - Ok(()) -} - -async fn extract_gz( - destination_path: &Path, - url: &str, - from: impl AsyncRead + Unpin, -) -> Result<(), anyhow::Error> { - let mut decompressed_bytes = GzipDecoder::new(BufReader::new(from)); - let mut file = smol::fs::File::create(&destination_path) - .await - .with_context(|| { - format!("creating a file {destination_path:?} for a download from {url}") - })?; - futures::io::copy(&mut decompressed_bytes, &mut file) - .await - .with_context(|| format!("extracting {url} to {destination_path:?}"))?; - Ok(()) -} - -struct HashingWriter { - writer: W, - hasher: Sha256, -} - -impl AsyncWrite for HashingWriter { - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - buf: &[u8], - ) -> Poll> { - match Pin::new(&mut self.writer).poll_write(cx, buf) { - Poll::Ready(Ok(n)) => { - self.hasher.update(&buf[..n]); - Poll::Ready(Ok(n)) - } - other => other, - } - } - - fn poll_flush( - mut self: Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> Poll> { - Pin::new(&mut self.writer).poll_flush(cx) - } - - fn poll_close( - mut self: Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> Poll> { - Pin::new(&mut self.writer).poll_close(cx) - } -} diff --git a/crates/languages/src/go.rs b/crates/languages/src/go.rs index 24e2ca2f56..16c1b67203 100644 --- a/crates/languages/src/go.rs +++ b/crates/languages/src/go.rs @@ -53,7 +53,7 @@ const BINARY: &str = if cfg!(target_os = "windows") { #[async_trait(?Send)] impl super::LspAdapter for GoLspAdapter { fn name(&self) -> LanguageServerName { - Self::SERVER_NAME + Self::SERVER_NAME.clone() } async fn fetch_latest_server_version( @@ -75,7 +75,7 @@ impl super::LspAdapter for GoLspAdapter { async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, - _: Option, + _: Arc, _: &AsyncApp, ) -> Option { let path = delegate.which(Self::SERVER_NAME.as_ref()).await?; @@ -131,19 +131,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 - && 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 { + if 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) @@ -452,7 +452,7 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option = runnables - .iter() - .flat_map(|r| &r.runnable.tags) - .map(|tag| tag.0.to_string()) - .collect(); - assert!( - tag_strings.contains(&"go-test".to_string()), - "Should find go-test tag, found: {:?}", - tag_strings - ); - assert!( - tag_strings.contains(&"go-subtest".to_string()), - "Should find go-subtest tag, found: {:?}", - tag_strings + runnables.len() == 2, + "Should find test function and subtest with double quotes, found: {}", + runnables.len() ); let buffer = cx.new(|cx| { @@ -904,299 +860,10 @@ mod tests { .collect() }); - let tag_strings: Vec = runnables - .iter() - .flat_map(|r| &r.runnable.tags) - .map(|tag| tag.0.to_string()) - .collect(); - assert!( - tag_strings.contains(&"go-test".to_string()), - "Should find go-test tag, found: {:?}", - tag_strings - ); - assert!( - tag_strings.contains(&"go-subtest".to_string()), - "Should find go-subtest tag, found: {:?}", - tag_strings - ); - } - - #[gpui::test] - fn test_go_table_test_slice_detection(cx: &mut TestAppContext) { - let language = language("go", tree_sitter_go::LANGUAGE.into()); - - let table_test = r#" - package main - - import "testing" - - func TestExample(t *testing.T) { - _ = "some random string" - - testCases := []struct{ - name string - anotherStr string - }{ - { - name: "test case 1", - anotherStr: "foo", - }, - { - name: "test case 2", - anotherStr: "bar", - }, - } - - notATableTest := []struct{ - name string - }{ - { - name: "some string", - }, - { - name: "some other string", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - // test code here - }) - } - } - "#; - - let buffer = - cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx)); - cx.executor().run_until_parked(); - - let runnables: Vec<_> = buffer.update(cx, |buffer, _| { - let snapshot = buffer.snapshot(); - snapshot.runnable_ranges(0..table_test.len()).collect() - }); - - let tag_strings: Vec = runnables - .iter() - .flat_map(|r| &r.runnable.tags) - .map(|tag| tag.0.to_string()) - .collect(); - - assert!( - tag_strings.contains(&"go-test".to_string()), - "Should find go-test tag, found: {:?}", - tag_strings - ); - assert!( - tag_strings.contains(&"go-table-test-case".to_string()), - "Should find go-table-test-case tag, found: {:?}", - tag_strings - ); - - let go_test_count = tag_strings.iter().filter(|&tag| tag == "go-test").count(); - let go_table_test_count = tag_strings - .iter() - .filter(|&tag| tag == "go-table-test-case") - .count(); - - assert!( - go_test_count == 1, - "Should find exactly 1 go-test, found: {}", - go_test_count - ); - assert!( - go_table_test_count == 2, - "Should find exactly 2 go-table-test-case, found: {}", - go_table_test_count - ); - } - - #[gpui::test] - fn test_go_table_test_slice_ignored(cx: &mut TestAppContext) { - let language = language("go", tree_sitter_go::LANGUAGE.into()); - - let table_test = r#" - package main - - func Example() { - _ = "some random string" - - notATableTest := []struct{ - name string - }{ - { - name: "some string", - }, - { - name: "some other string", - }, - } - } - "#; - - let buffer = - cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx)); - cx.executor().run_until_parked(); - - let runnables: Vec<_> = buffer.update(cx, |buffer, _| { - let snapshot = buffer.snapshot(); - snapshot.runnable_ranges(0..table_test.len()).collect() - }); - - let tag_strings: Vec = runnables - .iter() - .flat_map(|r| &r.runnable.tags) - .map(|tag| tag.0.to_string()) - .collect(); - - assert!( - !tag_strings.contains(&"go-test".to_string()), - "Should find go-test tag, found: {:?}", - tag_strings - ); - assert!( - !tag_strings.contains(&"go-table-test-case".to_string()), - "Should find go-table-test-case tag, found: {:?}", - tag_strings - ); - } - - #[gpui::test] - fn test_go_table_test_map_detection(cx: &mut TestAppContext) { - let language = language("go", tree_sitter_go::LANGUAGE.into()); - - let table_test = r#" - package main - - import "testing" - - func TestExample(t *testing.T) { - _ = "some random string" - - testCases := map[string]struct { - someStr string - fail bool - }{ - "test failure": { - someStr: "foo", - fail: true, - }, - "test success": { - someStr: "bar", - fail: false, - }, - } - - notATableTest := map[string]struct { - someStr string - }{ - "some string": { - someStr: "foo", - }, - "some other string": { - someStr: "bar", - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - // test code here - }) - } - } - "#; - - let buffer = - cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx)); - cx.executor().run_until_parked(); - - let runnables: Vec<_> = buffer.update(cx, |buffer, _| { - let snapshot = buffer.snapshot(); - snapshot.runnable_ranges(0..table_test.len()).collect() - }); - - let tag_strings: Vec = runnables - .iter() - .flat_map(|r| &r.runnable.tags) - .map(|tag| tag.0.to_string()) - .collect(); - - assert!( - tag_strings.contains(&"go-test".to_string()), - "Should find go-test tag, found: {:?}", - tag_strings - ); - assert!( - tag_strings.contains(&"go-table-test-case".to_string()), - "Should find go-table-test-case tag, found: {:?}", - tag_strings - ); - - let go_test_count = tag_strings.iter().filter(|&tag| tag == "go-test").count(); - let go_table_test_count = tag_strings - .iter() - .filter(|&tag| tag == "go-table-test-case") - .count(); - - assert!( - go_test_count == 1, - "Should find exactly 1 go-test, found: {}", - go_test_count - ); - assert!( - go_table_test_count == 2, - "Should find exactly 2 go-table-test-case, found: {}", - go_table_test_count - ); - } - - #[gpui::test] - fn test_go_table_test_map_ignored(cx: &mut TestAppContext) { - let language = language("go", tree_sitter_go::LANGUAGE.into()); - - let table_test = r#" - package main - - func Example() { - _ = "some random string" - - notATableTest := map[string]struct { - someStr string - }{ - "some string": { - someStr: "foo", - }, - "some other string": { - someStr: "bar", - }, - } - } - "#; - - let buffer = - cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx)); - cx.executor().run_until_parked(); - - let runnables: Vec<_> = buffer.update(cx, |buffer, _| { - let snapshot = buffer.snapshot(); - snapshot.runnable_ranges(0..table_test.len()).collect() - }); - - let tag_strings: Vec = runnables - .iter() - .flat_map(|r| &r.runnable.tags) - .map(|tag| tag.0.to_string()) - .collect(); - - assert!( - !tag_strings.contains(&"go-test".to_string()), - "Should find go-test tag, found: {:?}", - tag_strings - ); - assert!( - !tag_strings.contains(&"go-table-test-case".to_string()), - "Should find go-table-test-case tag, found: {:?}", - tag_strings + runnables.len() == 2, + "Should find test function and subtest with backticks, found: {}", + runnables.len() ); } diff --git a/crates/languages/src/go/outline.scm b/crates/languages/src/go/outline.scm index c745f55aff..e37ae7e572 100644 --- a/crates/languages/src/go/outline.scm +++ b/crates/languages/src/go/outline.scm @@ -1,5 +1,4 @@ (comment) @annotation - (type_declaration "type" @context [ @@ -43,13 +42,13 @@ (var_declaration "var" @context [ - ; The declaration may define multiple variables, and so @item is on - ; the identifier so they get distinct ranges. (var_spec - name: (identifier) @name @item) + name: (identifier) @name) @item (var_spec_list + "(" (var_spec - name: (identifier) @name @item) + name: (identifier) @name) @item + ")" ) ] ) @@ -61,7 +60,5 @@ "(" @context ")" @context)) @item -; Fields declarations may define multiple fields, and so @item is on the -; declarator so they each get distinct ranges. (field_declaration - name: (_) @name @item) + name: (_) @name) @item diff --git a/crates/languages/src/go/runnables.scm b/crates/languages/src/go/runnables.scm index f56262f799..6418cd04d8 100644 --- a/crates/languages/src/go/runnables.scm +++ b/crates/languages/src/go/runnables.scm @@ -91,103 +91,3 @@ ) @_ (#set! tag go-main) ) - -; Table test cases - slice and map -( - (short_var_declaration - left: (expression_list (identifier) @_collection_var) - right: (expression_list - (composite_literal - type: [ - (slice_type) - (map_type - key: (type_identifier) @_key_type - (#eq? @_key_type "string") - ) - ] - body: (literal_value - [ - (literal_element - (literal_value - (keyed_element - (literal_element - (identifier) @_field_name - ) - (literal_element - [ - (interpreted_string_literal) @run @_table_test_case_name - (raw_string_literal) @run @_table_test_case_name - ] - ) - ) - ) - ) - (keyed_element - (literal_element - [ - (interpreted_string_literal) @run @_table_test_case_name - (raw_string_literal) @run @_table_test_case_name - ] - ) - ) - ] - ) - ) - ) - ) - (for_statement - (range_clause - left: (expression_list - [ - ( - (identifier) - (identifier) @_loop_var - ) - (identifier) @_loop_var - ] - ) - right: (identifier) @_range_var - (#eq? @_range_var @_collection_var) - ) - body: (block - (expression_statement - (call_expression - function: (selector_expression - operand: (identifier) @_t_var - field: (field_identifier) @_run_method - (#eq? @_run_method "Run") - ) - arguments: (argument_list - . - [ - (selector_expression - operand: (identifier) @_tc_var - (#eq? @_tc_var @_loop_var) - field: (field_identifier) @_field_check - (#eq? @_field_check @_field_name) - ) - (identifier) @_arg_var - (#eq? @_arg_var @_loop_var) - ] - . - (func_literal - parameters: (parameter_list - (parameter_declaration - type: (pointer_type - (qualified_type - package: (package_identifier) @_pkg - name: (type_identifier) @_type - (#eq? @_pkg "testing") - (#eq? @_type "T") - ) - ) - ) - ) - ) - ) - ) - ) - ) - ) @_ - (#set! tag go-table-test-case) -) diff --git a/crates/languages/src/javascript/highlights.scm b/crates/languages/src/javascript/highlights.scm index ebeac7efff..73cb1a5e45 100644 --- a/crates/languages/src/javascript/highlights.scm +++ b/crates/languages/src/javascript/highlights.scm @@ -146,7 +146,6 @@ "&&=" "||=" "??=" - "..." ] @operator (regex "/" @string.regex) @@ -231,7 +230,6 @@ "implements" "interface" "keyof" - "module" "namespace" "private" "protected" @@ -251,4 +249,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 \ No newline at end of file diff --git a/crates/languages/src/javascript/injections.scm b/crates/languages/src/javascript/injections.scm index dbec1937b1..7baba5f227 100644 --- a/crates/languages/src/javascript/injections.scm +++ b/crates/languages/src/javascript/injections.scm @@ -11,21 +11,6 @@ (#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 diff --git a/crates/languages/src/javascript/outline.scm b/crates/languages/src/javascript/outline.scm index ca16c27a27..026c71e1f9 100644 --- a/crates/languages/src/javascript/outline.scm +++ b/crates/languages/src/javascript/outline.scm @@ -31,16 +31,12 @@ (export_statement (lexical_declaration ["let" "const"] @context - ; Multiple names may be exported - @item is on the declarator to keep - ; ranges distinct. (variable_declarator name: (_) @name) @item))) (program (lexical_declaration ["let" "const"] @context - ; Multiple names may be defined - @item is on the declarator to keep - ; ranges distinct. (variable_declarator name: (_) @name) @item)) diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 4fcf865568..15818730b8 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -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, LocalFile as _, LspAdapter, - LspAdapterDelegate, Toolchain, + ContextProvider, LanguageRegistry, LanguageToolchainStore, LocalFile as _, LspAdapter, + LspAdapterDelegate, }; use lsp::{LanguageServerBinary, LanguageServerName}; -use node_runtime::{NodeRuntime, VersionStrategy}; +use node_runtime::NodeRuntime; 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().iter().map(|&name| { + .extend(cx.all_action_names().into_iter().map(|&name| { project::lsp_store::json_language_server_ext::url_schema_for_action(name) })); @@ -269,18 +269,10 @@ impl JsonLspAdapter { .await; let config = cx.update(|cx| { - Self::get_workspace_config( - self.languages - .language_names() - .into_iter() - .map(|name| name.to_string()) - .collect(), - adapter_schemas, - cx, - ) + Self::get_workspace_config(self.languages.language_names().clone(), adapter_schemas, cx) })?; writer.replace(config.clone()); - Ok(config) + return Ok(config); } } @@ -303,7 +295,7 @@ impl LspAdapter for JsonLspAdapter { async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, - _: Option, + _: Arc, _: &AsyncApp, ) -> Option { let path = delegate @@ -340,12 +332,7 @@ impl LspAdapter for JsonLspAdapter { let should_install_language_server = self .node - .should_install_npm_package( - Self::PACKAGE_NAME, - &server_path, - container_dir, - VersionStrategy::Latest(version), - ) + .should_install_npm_package(Self::PACKAGE_NAME, &server_path, &container_dir, &version) .await; if should_install_language_server { @@ -404,7 +391,7 @@ impl LspAdapter for JsonLspAdapter { self: Arc, _: &dyn Fs, delegate: &Arc, - _: Option, + _: Arc, cx: &mut AsyncApp, ) -> Result { let mut config = self.get_or_init_workspace_config(cx).await?; @@ -421,10 +408,10 @@ impl LspAdapter for JsonLspAdapter { Ok(config) } - fn language_ids(&self) -> HashMap { + fn language_ids(&self) -> HashMap { [ - (LanguageName::new("JSON"), "json".into()), - (LanguageName::new("JSONC"), "jsonc".into()), + ("JSON".into(), "json".into()), + ("JSONC".into(), "jsonc".into()), ] .into_iter() .collect() @@ -488,7 +475,7 @@ impl NodeVersionAdapter { #[async_trait(?Send)] impl LspAdapter for NodeVersionAdapter { fn name(&self) -> LanguageServerName { - Self::SERVER_NAME + Self::SERVER_NAME.clone() } async fn fetch_latest_server_version( @@ -522,14 +509,13 @@ impl LspAdapter for NodeVersionAdapter { Ok(Box::new(GitHubLspBinaryVersion { name: release.tag_name, url: asset.browser_download_url.clone(), - digest: asset.digest.clone(), })) } async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, - _: Option, + _: Arc, _: &AsyncApp, ) -> Option { let path = delegate.which(Self::SERVER_NAME.as_ref()).await?; diff --git a/crates/languages/src/jsonc/overrides.scm b/crates/languages/src/jsonc/overrides.scm index 81fec9a5f5..cc966ad4c1 100644 --- a/crates/languages/src/jsonc/overrides.scm +++ b/crates/languages/src/jsonc/overrides.scm @@ -1,2 +1 @@ -(comment) @comment.inclusive (string) @string diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index d391e67d33..001fd15200 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -1,6 +1,6 @@ use anyhow::Context as _; use feature_flags::{FeatureFlag, FeatureFlagAppExt as _}; -use gpui::{App, SharedString, UpdateGlobal}; +use gpui::{App, UpdateGlobal}; use node_runtime::NodeRuntime; use python::PyprojectTomlManifestProvider; use rust::CargoManifestProvider; @@ -17,7 +17,6 @@ use crate::{json::JsonTaskProvider, python::BasedPyrightLspAdapter}; mod bash; mod c; mod css; -mod github_download; mod go; mod json; mod package_json; @@ -104,7 +103,7 @@ pub fn init(languages: Arc, node: NodeRuntime, cx: &mut App) { 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)); + let yaml_lsp_adapter = Arc::new(yaml::YamlLspAdapter::new(node.clone())); let built_in_languages = [ LanguageInfo { @@ -119,12 +118,12 @@ pub fn init(languages: Arc, node: NodeRuntime, cx: &mut App) { }, LanguageInfo { name: "cpp", - adapters: vec![c_lsp_adapter], + adapters: vec![c_lsp_adapter.clone()], ..Default::default() }, LanguageInfo { name: "css", - adapters: vec![css_lsp_adapter], + adapters: vec![css_lsp_adapter.clone()], ..Default::default() }, LanguageInfo { @@ -146,20 +145,20 @@ pub fn init(languages: Arc, node: NodeRuntime, cx: &mut App) { }, LanguageInfo { name: "gowork", - adapters: vec![go_lsp_adapter], - context: Some(go_context_provider), + adapters: vec![go_lsp_adapter.clone()], + context: Some(go_context_provider.clone()), ..Default::default() }, LanguageInfo { name: "json", - adapters: vec![json_lsp_adapter.clone(), node_version_lsp_adapter], + adapters: vec![json_lsp_adapter.clone(), node_version_lsp_adapter.clone()], context: Some(json_context_provider.clone()), ..Default::default() }, LanguageInfo { name: "jsonc", - adapters: vec![json_lsp_adapter], - context: Some(json_context_provider), + adapters: vec![json_lsp_adapter.clone()], + context: Some(json_context_provider.clone()), ..Default::default() }, LanguageInfo { @@ -174,16 +173,14 @@ pub fn init(languages: Arc, node: NodeRuntime, cx: &mut App) { }, LanguageInfo { name: "python", - adapters: vec![python_lsp_adapter, py_lsp_adapter], + adapters: vec![python_lsp_adapter.clone(), py_lsp_adapter.clone()], 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 { @@ -201,7 +198,7 @@ pub fn init(languages: Arc, node: NodeRuntime, cx: &mut App) { LanguageInfo { name: "javascript", adapters: vec![typescript_lsp_adapter.clone(), vtsls_adapter.clone()], - context: Some(typescript_context), + context: Some(typescript_context.clone()), ..Default::default() }, LanguageInfo { @@ -236,7 +233,6 @@ pub fn init(languages: Arc, node: NodeRuntime, cx: &mut App) { registration.adapters, registration.context, registration.toolchain, - registration.manifest_name, ); } @@ -244,8 +240,11 @@ pub fn init(languages: Arc, node: NodeRuntime, cx: &mut App) { cx.observe_flag::({ let languages = languages.clone(); move |enabled, _| { - if enabled && let Some(adapter) = basedpyright_lsp_adapter.take() { - languages.register_available_lsp_adapter(adapter.name(), move || adapter.clone()); + if enabled { + if let Some(adapter) = basedpyright_lsp_adapter.take() { + languages + .register_available_lsp_adapter(adapter.name(), move || adapter.clone()); + } } } }) @@ -277,13 +276,13 @@ pub fn init(languages: Arc, node: NodeRuntime, cx: &mut App) { move || adapter.clone() }); languages.register_available_lsp_adapter(LanguageServerName("vtsls".into()), { - let adapter = vtsls_adapter; + let adapter = vtsls_adapter.clone(); move || adapter.clone() }); languages.register_available_lsp_adapter( LanguageServerName("typescript-language-server".into()), { - let adapter = typescript_lsp_adapter; + let adapter = typescript_lsp_adapter.clone(); move || adapter.clone() }, ); @@ -340,7 +339,7 @@ pub fn init(languages: Arc, node: NodeRuntime, cx: &mut App) { Arc::from(PyprojectTomlManifestProvider), ]; for provider in manifest_providers { - project::ManifestProvidersStore::global(cx).register(provider); + project::ManifestProviders::global(cx).register(provider); } } @@ -350,7 +349,6 @@ struct LanguageInfo { adapters: Vec>, context: Option>, toolchain: Option>, - manifest_name: Option, } fn register_language( @@ -359,7 +357,6 @@ fn register_language( adapters: Vec>, context: Option>, toolchain: Option>, - manifest_name: Option, ) { let config = load_config(name); for adapter in adapters { @@ -370,14 +367,12 @@ 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(), }) }), ); diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index d21b5dabd3..0524c02fd5 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -4,16 +4,16 @@ use async_trait::async_trait; use collections::HashMap; use gpui::{App, Task}; use gpui::{AsyncApp, SharedString}; -use language::Toolchain; use language::ToolchainList; use language::ToolchainLister; 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 lsp::LanguageServerBinary; use lsp::LanguageServerName; -use node_runtime::{NodeRuntime, VersionStrategy}; +use node_runtime::NodeRuntime; use pet_core::Configuration; use pet_core::os_environment::Environment; use pet_core::python_environment::PythonEnvironmentKind; @@ -103,7 +103,7 @@ impl PythonLspAdapter { #[async_trait(?Send)] impl LspAdapter for PythonLspAdapter { fn name(&self) -> LanguageServerName { - Self::SERVER_NAME + Self::SERVER_NAME.clone() } async fn initialization_options( @@ -127,7 +127,7 @@ impl LspAdapter for PythonLspAdapter { async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, - _: Option, + _: Arc, _: &AsyncApp, ) -> Option { if let Some(pyright_bin) = delegate.which("pyright-langserver".as_ref()).await { @@ -204,8 +204,8 @@ impl LspAdapter for PythonLspAdapter { .should_install_npm_package( Self::SERVER_NAME.as_ref(), &server_path, - container_dir, - VersionStrategy::Latest(version), + &container_dir, + &version, ) .await; @@ -319,9 +319,17 @@ impl LspAdapter for PythonLspAdapter { self: Arc, _: &dyn Fs, adapter: &Arc, - toolchain: Option, + toolchains: Arc, cx: &mut AsyncApp, ) -> Result { + 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,31 +346,31 @@ impl LspAdapter for PythonLspAdapter { let interpreter_path = toolchain.path.to_string(); // Detect if this is a virtual environment - if let Some(interpreter_dir) = Path::new(&interpreter_path).parent() - && 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(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()), + ); + } } } } @@ -389,6 +397,12 @@ impl LspAdapter for PythonLspAdapter { user_settings }) } + fn manifest_name(&self) -> Option { + Some(SharedString::new_static("pyproject.toml").into()) + } + fn workspace_folders_content(&self) -> WorkspaceFoldersContent { + WorkspaceFoldersContent::WorktreeRoot + } } async fn get_cached_server_binary( @@ -711,7 +725,7 @@ impl Default for PythonToolchainProvider { } } -static ENV_PRIORITY_LIST: &[PythonEnvironmentKind] = &[ +static ENV_PRIORITY_LIST: &'static [PythonEnvironmentKind] = &[ // Prioritize non-Conda environments. PythonEnvironmentKind::Poetry, PythonEnvironmentKind::Pipenv, @@ -828,7 +842,7 @@ impl ToolchainLister for PythonToolchainProvider { .get_env_var("CONDA_PREFIX".to_string()) .map(|conda_prefix| { let is_match = |exe: &Option| { - exe.as_ref().is_some_and(|e| e.starts_with(&conda_prefix)) + exe.as_ref().map_or(false, |e| e.starts_with(&conda_prefix)) }; match (is_match(&lhs.executable), is_match(&rhs.executable)) { (true, false) => Ordering::Less, @@ -1026,14 +1040,14 @@ const BINARY_DIR: &str = if cfg!(target_os = "windows") { #[async_trait(?Send)] impl LspAdapter for PyLspAdapter { fn name(&self) -> LanguageServerName { - Self::SERVER_NAME + Self::SERVER_NAME.clone() } async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, - toolchain: Option, - _: &AsyncApp, + toolchains: Arc, + cx: &AsyncApp, ) -> Option { if let Some(pylsp_bin) = delegate.which(Self::SERVER_NAME.as_ref()).await { let env = delegate.shell_env().await; @@ -1043,7 +1057,14 @@ impl LspAdapter for PyLspAdapter { arguments: vec![], }) } else { - let venv = toolchain?; + 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"); pylsp_path.exists().then(|| LanguageServerBinary { path: venv.path.to_string().into(), @@ -1190,9 +1211,17 @@ impl LspAdapter for PyLspAdapter { self: Arc, _: &dyn Fs, adapter: &Arc, - toolchain: Option, + toolchains: Arc, cx: &mut AsyncApp, ) -> Result { + 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) @@ -1253,6 +1282,12 @@ impl LspAdapter for PyLspAdapter { user_settings }) } + fn manifest_name(&self) -> Option { + Some(SharedString::new_static("pyproject.toml").into()) + } + fn workspace_folders_content(&self) -> WorkspaceFoldersContent { + WorkspaceFoldersContent::WorktreeRoot + } } pub(crate) struct BasedPyrightLspAdapter { @@ -1318,7 +1353,7 @@ impl BasedPyrightLspAdapter { #[async_trait(?Send)] impl LspAdapter for BasedPyrightLspAdapter { fn name(&self) -> LanguageServerName { - Self::SERVER_NAME + Self::SERVER_NAME.clone() } async fn initialization_options( @@ -1342,8 +1377,8 @@ impl LspAdapter for BasedPyrightLspAdapter { async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, - toolchain: Option, - _: &AsyncApp, + toolchains: Arc, + cx: &AsyncApp, ) -> Option { if let Some(bin) = delegate.which(Self::BINARY_NAME.as_ref()).await { let env = delegate.shell_env().await; @@ -1353,7 +1388,15 @@ impl LspAdapter for BasedPyrightLspAdapter { arguments: vec!["--stdio".into()], }) } else { - let path = Path::new(toolchain?.path.as_ref()) + 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()) .parent()? .join(Self::BINARY_NAME); path.exists().then(|| LanguageServerBinary { @@ -1500,9 +1543,17 @@ impl LspAdapter for BasedPyrightLspAdapter { self: Arc, _: &dyn Fs, adapter: &Arc, - toolchain: Option, + toolchains: Arc, cx: &mut AsyncApp, ) -> Result { + 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) @@ -1519,31 +1570,31 @@ impl LspAdapter for BasedPyrightLspAdapter { let interpreter_path = toolchain.path.to_string(); // Detect if this is a virtual environment - if let Some(interpreter_dir) = Path::new(&interpreter_path).parent() - && 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(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()), + ); + } } } } @@ -1570,6 +1621,14 @@ impl LspAdapter for BasedPyrightLspAdapter { user_settings }) } + + fn manifest_name(&self) -> Option { + Some(SharedString::new_static("pyproject.toml").into()) + } + + fn workspace_folders_content(&self) -> WorkspaceFoldersContent { + WorkspaceFoldersContent::WorktreeRoot + } } #[cfg(test)] diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index 3e8dce756b..3f83c9c000 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -1,7 +1,8 @@ use anyhow::{Context as _, Result}; +use async_compression::futures::bufread::GzipDecoder; use async_trait::async_trait; use collections::HashMap; -use futures::StreamExt; +use futures::{StreamExt, io::BufReader}; use gpui::{App, AppContext, AsyncApp, SharedString, Task}; use http_client::github::AssetKind; use http_client::github::{GitHubLspBinaryVersion, latest_github_release}; @@ -15,7 +16,6 @@ use serde_json::json; use settings::Settings as _; use smol::fs::{self}; use std::fmt::Display; -use std::ops::Range; use std::{ any::Any, borrow::Cow, @@ -23,11 +23,14 @@ use std::{ sync::{Arc, LazyLock}, }; use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName}; -use util::fs::{make_file_executable, remove_matching}; +use util::archive::extract_zip; use util::merge_json_value_into; -use util::{ResultExt, maybe}; +use util::{ + ResultExt, + fs::{make_file_executable, remove_matching}, + maybe, +}; -use crate::github_download::{GithubBinaryMetadata, download_server_binary}; use crate::language_settings::language_settings; pub struct RustLspAdapter; @@ -106,13 +109,17 @@ impl ManifestProvider for CargoManifestProvider { #[async_trait(?Send)] impl LspAdapter for RustLspAdapter { fn name(&self) -> LanguageServerName { - SERVER_NAME + SERVER_NAME.clone() + } + + fn manifest_name(&self) -> Option { + Some(SharedString::new_static("Cargo.toml").into()) } async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, - _: Option, + _: Arc, _: &AsyncApp, ) -> Option { let path = delegate.which("rust-analyzer".as_ref()).await?; @@ -156,15 +163,15 @@ impl LspAdapter for RustLspAdapter { ) .await?; let asset_name = Self::build_asset_name(); + let asset = release .assets - .into_iter() + .iter() .find(|asset| asset.name == asset_name) .with_context(|| format!("no asset found matching `{asset_name:?}`"))?; Ok(Box::new(GitHubLspBinaryVersion { name: release.tag_name, - url: asset.browser_download_url, - digest: asset.digest, + url: asset.browser_download_url.clone(), })) } @@ -174,75 +181,57 @@ impl LspAdapter for RustLspAdapter { container_dir: PathBuf, delegate: &dyn LspAdapterDelegate, ) -> Result { - let GitHubLspBinaryVersion { - name, - url, - digest: expected_digest, - } = *version.downcast::().unwrap(); - let destination_path = container_dir.join(format!("rust-analyzer-{name}")); + let version = version.downcast::().unwrap(); + let destination_path = container_dir.join(format!("rust-analyzer-{}", version.name)); let server_path = match Self::GITHUB_ASSET_KIND { AssetKind::TarGz | AssetKind::Gz => destination_path.clone(), // Tar and gzip extract in place. AssetKind::Zip => destination_path.clone().join("rust-analyzer.exe"), // zip contains a .exe }; - let binary = LanguageServerBinary { - path: server_path.clone(), - env: None, - arguments: Default::default(), - }; + if fs::metadata(&server_path).await.is_err() { + remove_matching(&container_dir, |entry| entry != destination_path).await; - let metadata_path = destination_path.with_extension("metadata"); - let metadata = GithubBinaryMetadata::read_from_file(&metadata_path) - .await - .ok(); - if let Some(metadata) = metadata { - let validity_check = async || { - delegate - .try_exec(LanguageServerBinary { - path: server_path.clone(), - arguments: vec!["--version".into()], - env: None, - }) - .await - .inspect_err(|err| { - log::warn!("Unable to run {server_path:?} asset, redownloading: {err}",) - }) - }; - if let (Some(actual_digest), Some(expected_digest)) = - (&metadata.digest, &expected_digest) - { - if actual_digest == expected_digest { - if validity_check().await.is_ok() { - return Ok(binary); - } - } else { - log::info!( - "SHA-256 mismatch for {destination_path:?} asset, downloading new asset. Expected: {expected_digest}, Got: {actual_digest}" - ); + let mut response = delegate + .http_client() + .get(&version.url, Default::default(), true) + .await + .with_context(|| format!("downloading release from {}", version.url))?; + match Self::GITHUB_ASSET_KIND { + AssetKind::TarGz => { + let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut())); + let archive = async_tar::Archive::new(decompressed_bytes); + archive.unpack(&destination_path).await.with_context(|| { + format!("extracting {} to {:?}", version.url, destination_path) + })?; } - } else if validity_check().await.is_ok() { - return Ok(binary); - } - } + AssetKind::Gz => { + let mut decompressed_bytes = + GzipDecoder::new(BufReader::new(response.body_mut())); + let mut file = + fs::File::create(&destination_path).await.with_context(|| { + format!( + "creating a file {:?} for a download from {}", + destination_path, version.url, + ) + })?; + futures::io::copy(&mut decompressed_bytes, &mut file) + .await + .with_context(|| { + format!("extracting {} to {:?}", version.url, destination_path) + })?; + } + AssetKind::Zip => { + extract_zip(&destination_path, response.body_mut()) + .await + .with_context(|| { + format!("unzipping {} to {:?}", version.url, destination_path) + })?; + } + }; - download_server_binary( - delegate, - &url, - expected_digest.as_deref(), - &destination_path, - Self::GITHUB_ASSET_KIND, - ) - .await?; - make_file_executable(&server_path).await?; - remove_matching(&container_dir, |path| path != destination_path).await; - GithubBinaryMetadata::write_to_file( - &GithubBinaryMetadata { - metadata_version: 1, - digest: expected_digest, - }, - &metadata_path, - ) - .await?; + // todo("windows") + make_file_executable(&server_path).await?; + } Ok(LanguageServerBinary { path: server_path, @@ -302,62 +291,66 @@ impl LspAdapter for RustLspAdapter { completion: &lsp::CompletionItem, language: &Arc, ) -> Option { - // rust-analyzer calls these detail left and detail right in terms of where it expects things to be rendered - // this usually contains signatures of the thing to be completed - let detail_right = completion + let detail = completion .label_details .as_ref() - .and_then(|detail| detail.description.as_ref()) + .and_then(|detail| detail.detail.as_ref()) .or(completion.detail.as_ref()) .map(|detail| detail.trim()); - // this tends to contain alias and import information - let detail_left = completion + let function_signature = completion .label_details .as_ref() - .and_then(|detail| detail.detail.as_deref()); - let mk_label = |text: String, filter_range: &dyn Fn() -> Range, runs| { - let filter_range = completion - .filter_text - .as_deref() - .and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len())) - .or_else(|| { - text.find(&completion.label) - .map(|ix| ix..ix + completion.label.len()) - }) - .unwrap_or_else(filter_range); - - CodeLabel { - text, - runs, - filter_range, - } - }; - let mut label = match (detail_right, completion.kind) { - (Some(signature), Some(lsp::CompletionItemKind::FIELD)) => { + .and_then(|detail| detail.description.as_deref()) + .or(completion.detail.as_deref()); + match (detail, completion.kind) { + (Some(detail), Some(lsp::CompletionItemKind::FIELD)) => { let name = &completion.label; - let text = format!("{name}: {signature}"); + let text = format!("{name}: {detail}"); let prefix = "struct S { "; - let source = Rope::from_iter([prefix, &text, " }"]); + let source = Rope::from(format!("{prefix}{text} }}")); let runs = language.highlight_text(&source, prefix.len()..prefix.len() + text.len()); - mk_label(text, &|| 0..completion.label.len(), runs) + let filter_range = completion + .filter_text + .as_deref() + .and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len())) + .unwrap_or(0..name.len()); + return Some(CodeLabel { + text, + runs, + filter_range, + }); } ( - Some(signature), + Some(detail), Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE), ) if completion.insert_text_format != Some(lsp::InsertTextFormat::SNIPPET) => { let name = &completion.label; - let text = format!("{name}: {signature}",); + let text = format!( + "{}: {}", + name, + completion.detail.as_deref().unwrap_or(detail) + ); let prefix = "let "; - let source = Rope::from_iter([prefix, &text, " = ();"]); + let source = Rope::from(format!("{prefix}{text} = ();")); let runs = language.highlight_text(&source, prefix.len()..prefix.len() + text.len()); - mk_label(text, &|| 0..completion.label.len(), runs) + let filter_range = completion + .filter_text + .as_deref() + .and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len())) + .unwrap_or(0..name.len()); + return Some(CodeLabel { + text, + runs, + filter_range, + }); } ( - function_signature, + Some(detail), Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD), ) => { + static REGEX: LazyLock = LazyLock::new(|| Regex::new("\\(…?\\)").unwrap()); const FUNCTION_PREFIXES: [&str; 6] = [ "async fn", "async unsafe fn", @@ -366,62 +359,57 @@ impl LspAdapter for RustLspAdapter { "unsafe fn", "fn", ]; - let fn_prefixed = FUNCTION_PREFIXES.iter().find_map(|&prefix| { - function_signature? - .strip_prefix(prefix) - .map(|suffix| (prefix, suffix)) + // Is it function `async`? + let fn_keyword = FUNCTION_PREFIXES.iter().find_map(|prefix| { + function_signature.as_ref().and_then(|signature| { + signature + .strip_prefix(*prefix) + .map(|suffix| (*prefix, suffix)) + }) }); - let label = if let Some(label) = completion - .label - .strip_suffix("(…)") - .or_else(|| completion.label.strip_suffix("()")) - { - label - } else { - &completion.label - }; - - static FULL_SIGNATURE_REGEX: LazyLock = - LazyLock::new(|| Regex::new(r"fn (.?+)\(").expect("Failed to create REGEX")); - if let Some((function_signature, match_)) = function_signature - .filter(|it| it.contains(&label)) - .and_then(|it| Some((it, FULL_SIGNATURE_REGEX.find(it)?))) - { - let source = Rope::from(function_signature); - let runs = language.highlight_text(&source, 0..function_signature.len()); - mk_label( - function_signature.to_owned(), - &|| match_.range().start - 3..match_.range().end - 1, - runs, - ) - } else if let Some((prefix, suffix)) = fn_prefixed { - let text = format!("{label}{suffix}"); - let source = Rope::from_iter([prefix, " ", &text, " {}"]); + // fn keyword should be followed by opening parenthesis. + if let Some((prefix, suffix)) = fn_keyword { + let mut text = REGEX.replace(&completion.label, suffix).to_string(); + let source = Rope::from(format!("{prefix} {text} {{}}")); let run_start = prefix.len() + 1; let runs = language.highlight_text(&source, run_start..run_start + text.len()); - mk_label(text, &|| 0..label.len(), runs) + if detail.starts_with("(") { + text.push(' '); + text.push_str(&detail); + } + let filter_range = completion + .filter_text + .as_deref() + .and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len())) + .unwrap_or(0..completion.label.find('(').unwrap_or(text.len())); + return Some(CodeLabel { + filter_range, + text, + runs, + }); } else if completion .detail .as_ref() - .is_some_and(|detail| detail.starts_with("macro_rules! ")) + .map_or(false, |detail| detail.starts_with("macro_rules! ")) { let text = completion.label.clone(); let len = text.len(); let source = Rope::from(text.as_str()); let runs = language.highlight_text(&source, 0..len); - mk_label(text, &|| 0..completion.label.len(), runs) - } else if detail_left.is_none() { - return None; - } else { - mk_label( - completion.label.clone(), - &|| 0..completion.label.len(), - vec![], - ) + let filter_range = completion + .filter_text + .as_deref() + .and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len())) + .unwrap_or(0..len); + return Some(CodeLabel { + filter_range, + text, + runs, + }); } } - (_, kind) => { - let highlight_name = kind.and_then(|kind| match kind { + (_, Some(kind)) => { + let highlight_name = match kind { lsp::CompletionItemKind::STRUCT | lsp::CompletionItemKind::INTERFACE | lsp::CompletionItemKind::ENUM => Some("type"), @@ -431,35 +419,27 @@ impl LspAdapter for RustLspAdapter { Some("constant") } _ => None, - }); + }; - let label = completion.label.clone(); - let mut runs = vec![]; + let mut label = completion.label.clone(); + if let Some(detail) = detail.filter(|detail| detail.starts_with("(")) { + label.push(' '); + label.push_str(detail); + } + let mut label = CodeLabel::plain(label, completion.filter_text.as_deref()); if let Some(highlight_name) = highlight_name { let highlight_id = language.grammar()?.highlight_id_for_name(highlight_name)?; - runs.push(( - 0..label.rfind('(').unwrap_or(completion.label.len()), + label.runs.push(( + 0..label.text.rfind('(').unwrap_or(completion.label.len()), highlight_id, )); - } else if detail_left.is_none() { - return None; } - mk_label(label, &|| 0..completion.label.len(), runs) - } - }; - - if let Some(detail_left) = detail_left { - label.text.push(' '); - if !detail_left.starts_with('(') { - label.text.push('('); - } - label.text.push_str(detail_left); - if !detail_left.ends_with(')') { - label.text.push(')'); + return Some(label); } + _ => {} } - Some(label) + None } async fn label_for_symbol( @@ -468,22 +448,55 @@ impl LspAdapter for RustLspAdapter { kind: lsp::SymbolKind, language: &Arc, ) -> Option { - let (prefix, suffix) = match kind { - lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => ("fn ", " () {}"), - lsp::SymbolKind::STRUCT => ("struct ", " {}"), - lsp::SymbolKind::ENUM => ("enum ", " {}"), - lsp::SymbolKind::INTERFACE => ("trait ", " {}"), - lsp::SymbolKind::CONSTANT => ("const ", ": () = ();"), - lsp::SymbolKind::MODULE => ("mod ", " {}"), - lsp::SymbolKind::TYPE_PARAMETER => ("type ", " {}"), + let (text, filter_range, display_range) = match kind { + lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => { + let text = format!("fn {} () {{}}", name); + let filter_range = 3..3 + name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + lsp::SymbolKind::STRUCT => { + let text = format!("struct {} {{}}", name); + let filter_range = 7..7 + name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + lsp::SymbolKind::ENUM => { + let text = format!("enum {} {{}}", name); + let filter_range = 5..5 + name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + lsp::SymbolKind::INTERFACE => { + let text = format!("trait {} {{}}", name); + let filter_range = 6..6 + name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + lsp::SymbolKind::CONSTANT => { + let text = format!("const {}: () = ();", name); + let filter_range = 6..6 + name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + lsp::SymbolKind::MODULE => { + let text = format!("mod {} {{}}", name); + let filter_range = 4..4 + name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + lsp::SymbolKind::TYPE_PARAMETER => { + let text = format!("type {} {{}}", name); + let filter_range = 5..5 + name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } _ => return None, }; - let filter_range = prefix.len()..prefix.len() + name.len(); - let display_range = 0..filter_range.end; Some(CodeLabel { - runs: language.highlight_text(&Rope::from_iter([prefix, name, suffix]), display_range), - text: format!("{prefix}{name}"), + runs: language.highlight_text(&text.as_str().into(), display_range.clone()), + text: text[display_range].to_string(), filter_range, }) } @@ -496,7 +509,7 @@ impl LspAdapter for RustLspAdapter { let enable_lsp_tasks = ProjectSettings::get_global(cx) .lsp .get(&SERVER_NAME) - .is_some_and(|s| s.enable_lsp_tasks); + .map_or(false, |s| s.enable_lsp_tasks); if enable_lsp_tasks { let experimental = json!({ "runnables": { @@ -510,6 +523,20 @@ 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) } } @@ -567,7 +594,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) = @@ -584,14 +611,16 @@ impl ContextProvider for RustContextProvider { if let Some(path) = local_abs_path .as_deref() .and_then(|local_abs_path| local_abs_path.parent()) - && let Some(package_name) = - human_readable_package_name(path, project_env.as_ref()).await { - variables.insert(RUST_PACKAGE_TASK_VARIABLE.clone(), package_name); + if let Some(package_name) = + human_readable_package_name(path, project_env.as_ref()).await + { + 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([ @@ -645,7 +674,7 @@ impl ContextProvider for RustContextProvider { .variables .get(CUSTOM_TARGET_DIR) .cloned(); - let run_task_args = if let Some(package_to_run) = package_to_run { + let run_task_args = if let Some(package_to_run) = package_to_run.clone() { vec!["run".into(), "-p".into(), package_to_run] } else { vec!["run".into()] @@ -996,21 +1025,11 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option path, // Tar and gzip extract in place. - AssetKind::Zip => path.join("rust-analyzer.exe"), // zip contains a .exe - }; - anyhow::Ok(LanguageServerBinary { - path, + path: last.context("no cached binary")?, env: None, arguments: Default::default(), }) @@ -1150,7 +1169,7 @@ mod tests { kind: Some(lsp::CompletionItemKind::FUNCTION), label: "hello(…)".to_string(), label_details: Some(CompletionItemLabelDetails { - detail: Some("(use crate::foo)".into()), + detail: Some(" (use crate::foo)".into()), description: Some("async fn(&mut Option) -> Vec".to_string()), }), ..Default::default() @@ -1197,7 +1216,7 @@ mod tests { kind: Some(lsp::CompletionItemKind::FUNCTION), label: "hello(…)".to_string(), label_details: Some(CompletionItemLabelDetails { - detail: Some("(use crate::foo)".to_string()), + detail: Some(" (use crate::foo)".to_string()), description: Some("fn(&mut Option) -> Vec".to_string()), }), @@ -1220,35 +1239,6 @@ mod tests { }) ); - assert_eq!( - adapter - .label_for_completion( - &lsp::CompletionItem { - kind: Some(lsp::CompletionItemKind::FUNCTION), - label: "hello".to_string(), - label_details: Some(CompletionItemLabelDetails { - detail: Some("(use crate::foo)".to_string()), - description: Some("fn(&mut Option) -> Vec".to_string()), - }), - ..Default::default() - }, - &language - ) - .await, - Some(CodeLabel { - text: "hello(&mut Option) -> Vec (use crate::foo)".to_string(), - filter_range: 0..5, - runs: vec![ - (0..5, highlight_function), - (7..10, highlight_keyword), - (11..17, highlight_type), - (18..19, highlight_type), - (25..28, highlight_type), - (29..30, highlight_type), - ], - }) - ); - assert_eq!( adapter .label_for_completion( @@ -1266,46 +1256,9 @@ mod tests { ) .await, Some(CodeLabel { - text: "await.as_deref_mut(&mut self) -> IterMut<'_, T>".to_string(), + text: "await.as_deref_mut()".to_string(), filter_range: 6..18, - runs: vec![ - (6..18, HighlightId(2)), - (20..23, HighlightId(1)), - (33..40, HighlightId(0)), - (45..46, HighlightId(0)) - ], - }) - ); - - assert_eq!( - adapter - .label_for_completion( - &lsp::CompletionItem { - kind: Some(lsp::CompletionItemKind::METHOD), - label: "as_deref_mut()".to_string(), - filter_text: Some("as_deref_mut".to_string()), - label_details: Some(CompletionItemLabelDetails { - detail: None, - description: Some( - "pub fn as_deref_mut(&mut self) -> IterMut<'_, T>".to_string() - ), - }), - ..Default::default() - }, - &language - ) - .await, - Some(CodeLabel { - text: "pub fn as_deref_mut(&mut self) -> IterMut<'_, T>".to_string(), - filter_range: 7..19, - runs: vec![ - (0..3, HighlightId(1)), - (4..6, HighlightId(1)), - (7..19, HighlightId(2)), - (21..24, HighlightId(1)), - (34..41, HighlightId(0)), - (46..47, HighlightId(0)) - ], + runs: vec![], }) ); @@ -1554,7 +1507,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); } diff --git a/crates/languages/src/tailwind.rs b/crates/languages/src/tailwind.rs index 47eb254053..cb4e939083 100644 --- a/crates/languages/src/tailwind.rs +++ b/crates/languages/src/tailwind.rs @@ -3,9 +3,9 @@ use async_trait::async_trait; use collections::HashMap; use futures::StreamExt; use gpui::AsyncApp; -use language::{LanguageName, LspAdapter, LspAdapterDelegate, Toolchain}; +use language::{LanguageToolchainStore, LspAdapter, LspAdapterDelegate}; use lsp::{LanguageServerBinary, LanguageServerName}; -use node_runtime::{NodeRuntime, VersionStrategy}; +use node_runtime::NodeRuntime; 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 + Self::SERVER_NAME.clone() } async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, - _: Option, + _: Arc, _: &AsyncApp, ) -> Option { let path = delegate.which(Self::SERVER_NAME.as_ref()).await?; @@ -108,12 +108,7 @@ impl LspAdapter for TailwindLspAdapter { let should_install_language_server = self .node - .should_install_npm_package( - Self::PACKAGE_NAME, - &server_path, - container_dir, - VersionStrategy::Latest(version), - ) + .should_install_npm_package(Self::PACKAGE_NAME, &server_path, &container_dir, &version) .await; if should_install_language_server { @@ -155,7 +150,7 @@ impl LspAdapter for TailwindLspAdapter { self: Arc, _: &dyn Fs, delegate: &Arc, - _: Option, + _: Arc, cx: &mut AsyncApp, ) -> Result { let mut tailwind_user_settings = cx.update(|cx| { @@ -173,20 +168,20 @@ impl LspAdapter for TailwindLspAdapter { })) } - fn language_ids(&self) -> HashMap { + fn language_ids(&self) -> HashMap { HashMap::from_iter([ - (LanguageName::new("Astro"), "astro".to_string()), - (LanguageName::new("HTML"), "html".to_string()), - (LanguageName::new("CSS"), "css".to_string()), - (LanguageName::new("JavaScript"), "javascript".to_string()), - (LanguageName::new("TSX"), "typescriptreact".to_string()), - (LanguageName::new("Svelte"), "svelte".to_string()), - (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("PHP"), "php".to_string()), - (LanguageName::new("Vue.js"), "vue".to_string()), + ("Astro".to_string(), "astro".to_string()), + ("HTML".to_string(), "html".to_string()), + ("CSS".to_string(), "css".to_string()), + ("JavaScript".to_string(), "javascript".to_string()), + ("TSX".to_string(), "typescriptreact".to_string()), + ("Svelte".to_string(), "svelte".to_string()), + ("Elixir".to_string(), "phoenix-heex".to_string()), + ("HEEX".to_string(), "phoenix-heex".to_string()), + ("ERB".to_string(), "erb".to_string()), + ("HTML/ERB".to_string(), "erb".to_string()), + ("PHP".to_string(), "php".to_string()), + ("Vue.js".to_string(), "vue".to_string()), ]) } } diff --git a/crates/languages/src/tsx/highlights.scm b/crates/languages/src/tsx/highlights.scm index f7cb987831..e2837c61fd 100644 --- a/crates/languages/src/tsx/highlights.scm +++ b/crates/languages/src/tsx/highlights.scm @@ -146,7 +146,6 @@ "&&=" "||=" "??=" - "..." ] @operator (regex "/" @string.regex) @@ -237,7 +236,6 @@ "implements" "interface" "keyof" - "module" "namespace" "private" "protected" @@ -257,4 +255,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 \ No newline at end of file diff --git a/crates/languages/src/tsx/injections.scm b/crates/languages/src/tsx/injections.scm index 9eec01cc89..48da80995b 100644 --- a/crates/languages/src/tsx/injections.scm +++ b/crates/languages/src/tsx/injections.scm @@ -11,21 +11,6 @@ (#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 diff --git a/crates/languages/src/tsx/outline.scm b/crates/languages/src/tsx/outline.scm index f4261b9697..5dafe791e4 100644 --- a/crates/languages/src/tsx/outline.scm +++ b/crates/languages/src/tsx/outline.scm @@ -34,16 +34,12 @@ (export_statement (lexical_declaration ["let" "const"] @context - ; Multiple names may be exported - @item is on the declarator to keep - ; ranges distinct. (variable_declarator name: (_) @name) @item)) (program (lexical_declaration ["let" "const"] @context - ; Multiple names may be defined - @item is on the declarator to keep - ; ranges distinct. (variable_declarator name: (_) @name) @item)) diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index 77cf1a64f1..fb51544841 100644 --- a/crates/languages/src/typescript.rs +++ b/crates/languages/src/typescript.rs @@ -1,4 +1,6 @@ use anyhow::{Context as _, Result}; +use async_compression::futures::bufread::GzipDecoder; +use async_tar::Archive; use async_trait::async_trait; use chrono::{DateTime, Local}; use collections::HashMap; @@ -6,14 +8,13 @@ use futures::future::join_all; use gpui::{App, AppContext, AsyncApp, Task}; use http_client::github::{AssetKind, GitHubLspBinaryVersion, build_asset_url}; use language::{ - ContextLocation, ContextProvider, File, LanguageName, LanguageToolchainStore, LspAdapter, - LspAdapterDelegate, Toolchain, + ContextLocation, ContextProvider, File, LanguageToolchainStore, LspAdapter, LspAdapterDelegate, }; use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName}; -use node_runtime::{NodeRuntime, VersionStrategy}; +use node_runtime::NodeRuntime; use project::{Fs, lsp_store::language_server_settings}; use serde_json::{Value, json}; -use smol::{fs, lock::RwLock, stream::StreamExt}; +use smol::{fs, io::BufReader, lock::RwLock, stream::StreamExt}; use std::{ any::Any, borrow::Cow, @@ -22,10 +23,11 @@ use std::{ sync::Arc, }; use task::{TaskTemplate, TaskTemplates, VariableName}; +use util::archive::extract_zip; use util::merge_json_value_into; use util::{ResultExt, fs::remove_matching, maybe}; -use crate::{PackageJson, PackageJsonData, github_download::download_server_binary}; +use crate::{PackageJson, PackageJsonData}; #[derive(Debug)] pub(crate) struct TypeScriptContextProvider { @@ -341,10 +343,10 @@ async fn detect_package_manager( fs: Arc, package_json_data: Option, ) -> &'static str { - if let Some(package_json_data) = package_json_data - && let Some(package_manager) = package_json_data.package_manager - { - return package_manager; + if let Some(package_json_data) = package_json_data { + if 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,7 +559,7 @@ struct TypeScriptVersions { #[async_trait(?Send)] impl LspAdapter for TypeScriptLspAdapter { fn name(&self) -> LanguageServerName { - Self::SERVER_NAME + Self::SERVER_NAME.clone() } async fn fetch_latest_server_version( @@ -587,8 +589,8 @@ impl LspAdapter for TypeScriptLspAdapter { .should_install_npm_package( Self::PACKAGE_NAME, &server_path, - container_dir, - VersionStrategy::Latest(version.typescript_version.as_str()), + &container_dir, + version.typescript_version.as_str(), ) .await; @@ -722,7 +724,7 @@ impl LspAdapter for TypeScriptLspAdapter { self: Arc, _: &dyn Fs, delegate: &Arc, - _: Option, + _: Arc, cx: &mut AsyncApp, ) -> Result { let override_options = cx.update(|cx| { @@ -739,11 +741,11 @@ impl LspAdapter for TypeScriptLspAdapter { })) } - fn language_ids(&self) -> HashMap { + fn language_ids(&self) -> HashMap { HashMap::from_iter([ - (LanguageName::new("TypeScript"), "typescript".into()), - (LanguageName::new("JavaScript"), "javascript".into()), - (LanguageName::new("TSX"), "typescriptreact".into()), + ("TypeScript".into(), "typescript".into()), + ("JavaScript".into(), "javascript".into()), + ("TSX".into(), "typescriptreact".into()), ]) } } @@ -822,7 +824,7 @@ impl LspAdapter for EsLintLspAdapter { self: Arc, _: &dyn Fs, delegate: &Arc, - _: Option, + _: Arc, cx: &mut AsyncApp, ) -> Result { let workspace_root = delegate.worktree_root_path(); @@ -879,7 +881,7 @@ impl LspAdapter for EsLintLspAdapter { } fn name(&self) -> LanguageServerName { - Self::SERVER_NAME + Self::SERVER_NAME.clone() } async fn fetch_latest_server_version( @@ -894,7 +896,6 @@ impl LspAdapter for EsLintLspAdapter { Ok(Box::new(GitHubLspBinaryVersion { name: Self::CURRENT_VERSION.into(), - digest: None, url, })) } @@ -910,16 +911,45 @@ 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, |_| true).await; + remove_matching(&container_dir, |entry| entry != destination_path).await; - download_server_binary( - delegate, - &version.url, - None, - &destination_path, - Self::GITHUB_ASSET_KIND, - ) - .await?; + let mut response = delegate + .http_client() + .get(&version.url, Default::default(), true) + .await + .context("downloading release")?; + match Self::GITHUB_ASSET_KIND { + AssetKind::TarGz => { + let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut())); + let archive = Archive::new(decompressed_bytes); + archive.unpack(&destination_path).await.with_context(|| { + format!("extracting {} to {:?}", version.url, destination_path) + })?; + } + AssetKind::Gz => { + let mut decompressed_bytes = + GzipDecoder::new(BufReader::new(response.body_mut())); + let mut file = + fs::File::create(&destination_path).await.with_context(|| { + format!( + "creating a file {:?} for a download from {}", + destination_path, version.url, + ) + })?; + futures::io::copy(&mut decompressed_bytes, &mut file) + .await + .with_context(|| { + format!("extracting {} to {:?}", version.url, destination_path) + })?; + } + AssetKind::Zip => { + extract_zip(&destination_path, response.body_mut()) + .await + .with_context(|| { + format!("unzipping {} to {:?}", version.url, destination_path) + })?; + } + } let mut dir = fs::read_dir(&destination_path).await?; let first = dir.next().await.context("missing first file")??; diff --git a/crates/languages/src/typescript/highlights.scm b/crates/languages/src/typescript/highlights.scm index 84cbbae77d..486e5a7684 100644 --- a/crates/languages/src/typescript/highlights.scm +++ b/crates/languages/src/typescript/highlights.scm @@ -167,7 +167,6 @@ "&&=" "||=" "??=" - "..." ] @operator (regex "/" @string.regex) @@ -248,7 +247,6 @@ "is" "keyof" "let" - "module" "namespace" "new" "of" @@ -273,4 +271,4 @@ "while" "with" "yield" -] @keyword +] @keyword \ No newline at end of file diff --git a/crates/languages/src/typescript/injections.scm b/crates/languages/src/typescript/injections.scm index 1ca1e9ad59..7affdc5b75 100644 --- a/crates/languages/src/typescript/injections.scm +++ b/crates/languages/src/typescript/injections.scm @@ -15,21 +15,6 @@ (#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 diff --git a/crates/languages/src/typescript/outline.scm b/crates/languages/src/typescript/outline.scm index f4261b9697..5dafe791e4 100644 --- a/crates/languages/src/typescript/outline.scm +++ b/crates/languages/src/typescript/outline.scm @@ -34,16 +34,12 @@ (export_statement (lexical_declaration ["let" "const"] @context - ; Multiple names may be exported - @item is on the declarator to keep - ; ranges distinct. (variable_declarator name: (_) @name) @item)) (program (lexical_declaration ["let" "const"] @context - ; Multiple names may be defined - @item is on the declarator to keep - ; ranges distinct. (variable_declarator name: (_) @name) @item)) diff --git a/crates/languages/src/vtsls.rs b/crates/languages/src/vtsls.rs index f7152b0b5d..ca07673d5f 100644 --- a/crates/languages/src/vtsls.rs +++ b/crates/languages/src/vtsls.rs @@ -2,9 +2,9 @@ use anyhow::Result; use async_trait::async_trait; use collections::HashMap; use gpui::AsyncApp; -use language::{LanguageName, LspAdapter, LspAdapterDelegate, Toolchain}; +use language::{LanguageToolchainStore, LspAdapter, LspAdapterDelegate}; use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName}; -use node_runtime::{NodeRuntime, VersionStrategy}; +use node_runtime::NodeRuntime; use project::{Fs, lsp_store::language_server_settings}; use serde_json::Value; use std::{ @@ -67,7 +67,7 @@ const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("vtsls"); #[async_trait(?Send)] impl LspAdapter for VtslsLspAdapter { fn name(&self) -> LanguageServerName { - SERVER_NAME + SERVER_NAME.clone() } async fn fetch_latest_server_version( @@ -86,7 +86,7 @@ impl LspAdapter for VtslsLspAdapter { async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, - _: Option, + _: Arc, _: &AsyncApp, ) -> Option { let env = delegate.shell_env().await; @@ -115,7 +115,7 @@ impl LspAdapter for VtslsLspAdapter { Self::PACKAGE_NAME, &server_path, &container_dir, - VersionStrategy::Latest(&latest_version.server_version), + &latest_version.server_version, ) .await { @@ -128,7 +128,7 @@ impl LspAdapter for VtslsLspAdapter { Self::TYPESCRIPT_PACKAGE_NAME, &container_dir.join(Self::TYPESCRIPT_TSDK_PATH), &container_dir, - VersionStrategy::Latest(&latest_version.typescript_version), + &latest_version.typescript_version, ) .await { @@ -211,7 +211,7 @@ impl LspAdapter for VtslsLspAdapter { self: Arc, fs: &dyn Fs, delegate: &Arc, - _: Option, + _: Arc, cx: &mut AsyncApp, ) -> Result { let tsdk_path = Self::tsdk_path(fs, delegate).await; @@ -273,11 +273,11 @@ impl LspAdapter for VtslsLspAdapter { Ok(default_workspace_configuration) } - fn language_ids(&self) -> HashMap { + fn language_ids(&self) -> HashMap { HashMap::from_iter([ - (LanguageName::new("TypeScript"), "typescript".into()), - (LanguageName::new("JavaScript"), "javascript".into()), - (LanguageName::new("TSX"), "typescriptreact".into()), + ("TypeScript".into(), "typescript".into()), + ("JavaScript".into(), "javascript".into()), + ("TSX".into(), "typescriptreact".into()), ]) } } diff --git a/crates/languages/src/yaml.rs b/crates/languages/src/yaml.rs index b9197b12ae..815605d524 100644 --- a/crates/languages/src/yaml.rs +++ b/crates/languages/src/yaml.rs @@ -2,9 +2,11 @@ use anyhow::{Context as _, Result}; use async_trait::async_trait; use futures::StreamExt; use gpui::AsyncApp; -use language::{LspAdapter, LspAdapterDelegate, Toolchain, language_settings::AllLanguageSettings}; +use language::{ + LanguageToolchainStore, LspAdapter, LspAdapterDelegate, language_settings::AllLanguageSettings, +}; use lsp::{LanguageServerBinary, LanguageServerName}; -use node_runtime::{NodeRuntime, VersionStrategy}; +use node_runtime::NodeRuntime; use project::{Fs, lsp_store::language_server_settings}; use serde_json::Value; use settings::{Settings, SettingsLocation}; @@ -38,7 +40,7 @@ impl YamlLspAdapter { #[async_trait(?Send)] impl LspAdapter for YamlLspAdapter { fn name(&self) -> LanguageServerName { - Self::SERVER_NAME + Self::SERVER_NAME.clone() } async fn fetch_latest_server_version( @@ -55,7 +57,7 @@ impl LspAdapter for YamlLspAdapter { async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, - _: Option, + _: Arc, _: &AsyncApp, ) -> Option { let path = delegate.which(Self::SERVER_NAME.as_ref()).await?; @@ -102,12 +104,7 @@ impl LspAdapter for YamlLspAdapter { let should_install_language_server = self .node - .should_install_npm_package( - Self::PACKAGE_NAME, - &server_path, - container_dir, - VersionStrategy::Latest(version), - ) + .should_install_npm_package(Self::PACKAGE_NAME, &server_path, &container_dir, &version) .await; if should_install_language_server { @@ -133,7 +130,7 @@ impl LspAdapter for YamlLspAdapter { self: Arc, _: &dyn Fs, delegate: &Arc, - _: Option, + _: Arc, cx: &mut AsyncApp, ) -> Result { let location = SettingsLocation { diff --git a/crates/languages/src/yaml/config.toml b/crates/languages/src/yaml/config.toml index e54bceda1a..4dfb890c54 100644 --- a/crates/languages/src/yaml/config.toml +++ b/crates/languages/src/yaml/config.toml @@ -1,6 +1,6 @@ name = "YAML" grammar = "yaml" -path_suffixes = ["yml", "yaml", "pixi.lock"] +path_suffixes = ["yml", "yaml"] line_comments = ["# "] autoclose_before = ",]}" brackets = [ diff --git a/crates/languages/src/yaml/outline.scm b/crates/languages/src/yaml/outline.scm index c5a7f8e5d4..7ab007835f 100644 --- a/crates/languages/src/yaml/outline.scm +++ b/crates/languages/src/yaml/outline.scm @@ -1,9 +1 @@ -(block_mapping_pair - key: - (flow_node - (plain_scalar - (string_scalar) @name)) - value: - (flow_node - (plain_scalar - (string_scalar) @context))?) @item +(block_mapping_pair key: (flow_node (plain_scalar (string_scalar) @name))) @item diff --git a/crates/languages/src/yaml/overrides.scm b/crates/languages/src/yaml/overrides.scm deleted file mode 100644 index 9503051a62..0000000000 --- a/crates/languages/src/yaml/overrides.scm +++ /dev/null @@ -1,5 +0,0 @@ -(comment) @comment.inclusive -[ - (single_quote_scalar) - (double_quote_scalar) -] @string diff --git a/crates/livekit_client/Cargo.toml b/crates/livekit_client/Cargo.toml index 3575325ac0..821fd5d390 100644 --- a/crates/livekit_client/Cargo.toml +++ b/crates/livekit_client/Cargo.toml @@ -25,7 +25,6 @@ async-trait.workspace = true collections.workspace = true cpal.workspace = true futures.workspace = true -audio.workspace = true gpui = { workspace = true, features = ["screen-capture", "x11", "wayland", "windows-manifest"] } gpui_tokio.workspace = true http_client_tls.workspace = true @@ -36,13 +35,10 @@ nanoid.workspace = true parking_lot.workspace = true postage.workspace = true smallvec.workspace = true -settings.workspace = true tokio-tungstenite.workspace = true util.workspace = true workspace-hack.workspace = true -rodio = { workspace = true, features = ["wav_output"] } - [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" } livekit = { rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d", git = "https://github.com/zed-industries/livekit-rust-sdks", features = [ diff --git a/crates/livekit_client/examples/test_app.rs b/crates/livekit_client/examples/test_app.rs index 7580642990..e1d01df534 100644 --- a/crates/livekit_client/examples/test_app.rs +++ b/crates/livekit_client/examples/test_app.rs @@ -159,14 +159,14 @@ impl LivekitWindow { if output .audio_output_stream .as_ref() - .is_some_and(|(track, _)| track.sid() == unpublish_sid) + .map_or(false, |(track, _)| track.sid() == unpublish_sid) { output.audio_output_stream.take(); } if output .screen_share_output_view .as_ref() - .is_some_and(|(track, _)| track.sid() == unpublish_sid) + .map_or(false, |(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, + publication.clone(), room.play_remote_audio_track(&track, cx).unwrap(), )); } diff --git a/crates/livekit_client/src/lib.rs b/crates/livekit_client/src/lib.rs index 055aa3704e..149859fdc8 100644 --- a/crates/livekit_client/src/lib.rs +++ b/crates/livekit_client/src/lib.rs @@ -1,13 +1,7 @@ -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, @@ -24,11 +18,6 @@ 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", @@ -179,59 +168,3 @@ 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> { - match sample_format { - cpal::SampleFormat::I8 => Ok(convert_sample_data::(data)), - cpal::SampleFormat::I16 => Ok(data.as_slice::().unwrap().to_vec()), - cpal::SampleFormat::I24 => Ok(convert_sample_data::(data)), - cpal::SampleFormat::I32 => Ok(convert_sample_data::(data)), - cpal::SampleFormat::I64 => Ok(convert_sample_data::(data)), - cpal::SampleFormat::U8 => Ok(convert_sample_data::(data)), - cpal::SampleFormat::U16 => Ok(convert_sample_data::(data)), - cpal::SampleFormat::U32 => Ok(convert_sample_data::(data)), - cpal::SampleFormat::U64 => Ok(convert_sample_data::(data)), - cpal::SampleFormat::F32 => Ok(convert_sample_data::(data)), - cpal::SampleFormat::F64 => Ok(convert_sample_data::(data)), - _ => anyhow::bail!("Unsupported sample format"), - } -} - -pub(crate) fn convert_sample_data< - TSource: cpal::SizedSample, - TDest: cpal::SizedSample + cpal::FromSample, ->( - data: &cpal::Data, -) -> Vec { - data.as_slice::() - .unwrap() - .iter() - .map(|e| e.to_sample::()) - .collect() -} diff --git a/crates/livekit_client/src/livekit_client.rs b/crates/livekit_client/src/livekit_client.rs index 0751b014f4..8f0ac1a456 100644 --- a/crates/livekit_client/src/livekit_client.rs +++ b/crates/livekit_client/src/livekit_client.rs @@ -1,14 +1,11 @@ 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; @@ -126,14 +123,9 @@ impl Room { pub fn play_remote_audio_track( &self, track: &RemoteAudioTrack, - cx: &mut App, + _cx: &App, ) -> Result { - if AudioSettings::get_global(cx).rodio_audio { - info!("Using experimental.rodio_audio audio pipeline"); - playback::play_remote_audio_track(&track.0, cx) - } else { - Ok(self.playback.play_remote_audio_track(&track.0)) - } + Ok(self.playback.play_remote_audio_track(&track.0)) } } diff --git a/crates/livekit_client/src/livekit_client/playback.rs b/crates/livekit_client/src/livekit_client/playback.rs index d6b64dbaca..c62b8853b4 100644 --- a/crates/livekit_client/src/livekit_client/playback.rs +++ b/crates/livekit_client/src/livekit_client/playback.rs @@ -1,6 +1,6 @@ use anyhow::{Context as _, Result}; -use cpal::traits::{DeviceTrait, StreamTrait as _}; +use cpal::traits::{DeviceTrait, HostTrait, StreamTrait as _}; use futures::channel::mpsc::UnboundedSender; use futures::{Stream, StreamExt as _}; use gpui::{ @@ -18,16 +18,13 @@ use livekit::webrtc::{ video_stream::native::NativeVideoStream, }; use parking_lot::Mutex; -use rodio::Source; use std::cell::RefCell; use std::sync::Weak; -use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; +use std::sync::atomic::{self, AtomicI32}; 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>, @@ -43,29 +40,6 @@ pub(crate) struct AudioStack { 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 { - let stop_handle = Arc::new(AtomicBool::new(false)); - let stop_handle_clone = stop_handle.clone(); - let stream = source::LiveKitStream::new(cx.background_executor(), track) - .stoppable() - .periodic_access(Duration::from_millis(50), move |s| { - if stop_handle.load(Ordering::Relaxed) { - s.stop(); - } - }); - audio::Audio::play_source(stream, 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 { let apm = Arc::new(Mutex::new(apm::AudioProcessingModule::new( @@ -87,7 +61,7 @@ impl AudioStack { ) -> AudioStream { let output_task = self.start_output(); - let next_ssrc = self.next_ssrc.fetch_add(1, Ordering::Relaxed); + let next_ssrc = self.next_ssrc.fetch_add(1, atomic::Ordering::Relaxed); let source = AudioMixerSource { ssrc: next_ssrc, sample_rate: SAMPLE_RATE, @@ -123,23 +97,6 @@ impl AudioStack { } } - fn start_output(&self) -> Arc> { - 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 - } - pub(crate) fn capture_local_microphone_track( &self, ) -> Result<(crate::LocalAudioTrack, AudioStream)> { @@ -160,6 +117,7 @@ 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(); @@ -174,12 +132,29 @@ impl AudioStack { drop(transmit_task); drop(capture_task); }); - Ok(( + return Ok(( super::LocalAudioTrack(track), AudioStream::Output { _drop: Box::new(on_drop), }, - )) + )); + } + + fn start_output(&self) -> Arc> { + 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( @@ -190,7 +165,7 @@ impl AudioStack { ) -> Result<()> { loop { let mut device_change_listener = DeviceChangeListener::new(false)?; - let (output_device, output_config) = crate::default_device(false)?; + let (output_device, output_config) = default_device(false)?; let (end_on_drop_tx, end_on_drop_rx) = std::sync::mpsc::channel::<()>(); let mixer = mixer.clone(); let apm = apm.clone(); @@ -262,7 +237,7 @@ impl AudioStack { ) -> Result<()> { loop { let mut device_change_listener = DeviceChangeListener::new(true)?; - let (device, config) = crate::default_device(true)?; + let (device, config) = 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(); @@ -283,15 +258,9 @@ impl AudioStack { let stream = device .build_input_stream_raw( &config.config(), - config.sample_format(), + cpal::SampleFormat::I16, move |data, _: &_| { - let data = - crate::get_sample_data(config.sample_format(), data).log_err(); - let Some(data) = data else { - return; - }; - let mut data = data.as_slice(); - + let mut data = data.as_slice::().unwrap(); while data.len() > 0 { let remainder = (buf.capacity() - buf.len()).min(data.len()); buf.extend_from_slice(&data[..remainder]); @@ -390,6 +359,27 @@ 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, diff --git a/crates/livekit_client/src/livekit_client/playback/source.rs b/crates/livekit_client/src/livekit_client/playback/source.rs deleted file mode 100644 index 021640247d..0000000000 --- a/crates/livekit_client/src/livekit_client/playback/source.rs +++ /dev/null @@ -1,67 +0,0 @@ -use futures::StreamExt; -use libwebrtc::{audio_stream::native::NativeAudioStream, prelude::AudioFrame}; -use livekit::track::RemoteAudioTrack; -use rodio::{Source, buffer::SamplesBuffer, conversions::SampleTypeConverter}; - -use crate::livekit_client::playback::{NUM_CHANNELS, SAMPLE_RATE}; - -fn frame_to_samplesbuffer(frame: AudioFrame) -> SamplesBuffer { - let samples = frame.data.iter().copied(); - let samples = SampleTypeConverter::<_, _>::new(samples); - let samples: Vec = samples.collect(); - SamplesBuffer::new(frame.num_channels as u16, frame.sample_rate, 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 as i32, NUM_CHANNELS as i32); - 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.inner.next() - } -} - -impl Source for LiveKitStream { - fn current_span_len(&self) -> Option { - self.inner.current_span_len() - } - - fn channels(&self) -> rodio::ChannelCount { - self.inner.channels() - } - - fn sample_rate(&self) -> rodio::SampleRate { - self.inner.sample_rate() - } - - fn total_duration(&self) -> Option { - self.inner.total_duration() - } -} diff --git a/crates/livekit_client/src/record.rs b/crates/livekit_client/src/record.rs deleted file mode 100644 index 925c0d4c67..0000000000 --- a/crates/livekit_client/src/record.rs +++ /dev/null @@ -1,91 +0,0 @@ -use std::{ - env, - 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>>, - _stream: cpal::Stream, -} - -impl CaptureInput { - pub fn start() -> anyhow::Result { - let (device, config) = crate::default_device(true)?; - let name = device.name().unwrap_or("".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 { - 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>>, -) -> Result { - 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>>, - 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 = SampleTypeConverter::<_, f32>::new(samples.into_iter()).collect(); - let mut samples = SamplesBuffer::new(config.channels(), config.sample_rate().0, samples); - match rodio::output_to_wav(&mut samples, path) { - Ok(_) => Ok(()), - Err(e) => Err(anyhow::anyhow!("Failed to write wav file: {}", e)), - } -} diff --git a/crates/livekit_client/src/test.rs b/crates/livekit_client/src/test.rs index 873e0222d0..e02c4d876f 100644 --- a/crates/livekit_client/src/test.rs +++ b/crates/livekit_client/src/test.rs @@ -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 { - 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(); @@ -736,14 +736,14 @@ impl Room { impl Drop for RoomState { fn drop(&mut self) { - 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(); + 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(); + } } } } diff --git a/crates/lsp/src/input_handler.rs b/crates/lsp/src/input_handler.rs index 001ebf1fc9..db3f1190fc 100644 --- a/crates/lsp/src/input_handler.rs +++ b/crates/lsp/src/input_handler.rs @@ -13,15 +13,14 @@ use parking_lot::Mutex; use smol::io::BufReader; use crate::{ - AnyResponse, CONTENT_LEN_HEADER, IoHandler, IoKind, NotificationOrRequest, RequestId, - ResponseHandler, + AnyNotification, AnyResponse, CONTENT_LEN_HEADER, IoHandler, IoKind, RequestId, ResponseHandler, }; const HEADER_DELIMITER: &[u8; 4] = b"\r\n\r\n"; /// Handler for stdout of language server. pub struct LspStdoutHandler { pub(super) loop_handle: Task>, - pub(super) incoming_messages: UnboundedReceiver, + pub(super) notifications_channel: UnboundedReceiver, } async fn read_headers(reader: &mut BufReader, buffer: &mut Vec) -> Result<()> @@ -55,13 +54,13 @@ impl LspStdoutHandler { let loop_handle = cx.spawn(Self::handler(stdout, tx, response_handlers, io_handlers)); Self { loop_handle, - incoming_messages: notifications_channel, + notifications_channel, } } async fn handler( stdout: Input, - notifications_sender: UnboundedSender, + notifications_sender: UnboundedSender, response_handlers: Arc>>>, io_handlers: Arc>>, ) -> anyhow::Result<()> @@ -97,7 +96,7 @@ impl LspStdoutHandler { } } - if let Ok(msg) = serde_json::from_slice::(&buffer) { + if let Ok(msg) = serde_json::from_slice::(&buffer) { notifications_sender.unbounded_send(msg)?; } else if let Ok(AnyResponse { id, error, result, .. diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 942225d098..b9701a83d2 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -45,7 +45,7 @@ use util::{ConnectionResult, ResultExt, TryFutureExt, redact}; const JSON_RPC_VERSION: &str = "2.0"; const CONTENT_LEN_HEADER: &str = "Content-Length: "; -pub const LSP_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 2); +const LSP_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 2); const SERVER_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5); type NotificationHandler = Box, Value, &mut AsyncApp)>; @@ -242,7 +242,7 @@ struct Notification<'a, T> { /// Language server RPC notification message before it is deserialized into a concrete type. #[derive(Debug, Clone, Deserialize)] -struct NotificationOrRequest { +struct AnyNotification { #[serde(default)] id: Option, method: String, @@ -252,10 +252,7 @@ struct NotificationOrRequest { #[derive(Debug, Serialize, Deserialize)] struct Error { - code: i64, message: String, - #[serde(default)] - data: Option, } pub trait LspRequestFuture: Future> { @@ -318,8 +315,6 @@ impl LanguageServer { } else { root_path.parent().unwrap_or_else(|| Path::new("/")) }; - let root_uri = Url::from_file_path(&working_dir) - .map_err(|()| anyhow!("{working_dir:?} is not a valid URI"))?; log::info!( "starting language server process. binary path: {:?}, working directory: {:?}, args: {:?}", @@ -347,6 +342,8 @@ impl LanguageServer { let stdin = server.stdin.take().unwrap(); let stdout = server.stdout.take().unwrap(); let stderr = server.stderr.take().unwrap(); + let root_uri = Url::from_file_path(&working_dir) + .map_err(|()| anyhow!("{working_dir:?} is not a valid URI"))?; let server = Self::new_internal( server_id, server_name, @@ -367,7 +364,6 @@ impl LanguageServer { notification.method, serde_json::to_string_pretty(¬ification.params).unwrap(), ); - false }, ); @@ -393,7 +389,7 @@ impl LanguageServer { Stdin: AsyncWrite + Unpin + Send + 'static, Stdout: AsyncRead + Unpin + Send + 'static, Stderr: AsyncRead + Unpin + Send + 'static, - F: Fn(&NotificationOrRequest) -> bool + 'static + Send + Sync + Clone, + F: FnMut(AnyNotification) + 'static + Send + Sync + Clone, { let (outbound_tx, outbound_rx) = channel::unbounded::(); let (output_done_tx, output_done_rx) = barrier::channel(); @@ -404,34 +400,14 @@ impl LanguageServer { let io_handlers = Arc::new(Mutex::new(HashMap::default())); let stdout_input_task = cx.spawn({ - let unhandled_notification_wrapper = { - let response_channel = outbound_tx.clone(); - async move |msg: NotificationOrRequest| { - let did_handle = on_unhandled_notification(&msg); - if !did_handle && let Some(message_id) = msg.id { - let response = AnyResponse { - jsonrpc: JSON_RPC_VERSION, - id: message_id, - error: Some(Error { - code: -32601, - message: format!("Unrecognized method `{}`", msg.method), - data: None, - }), - result: None, - }; - if let Ok(response) = serde_json::to_string(&response) { - response_channel.send(response).await.ok(); - } - } - } - }; + let on_unhandled_notification = on_unhandled_notification.clone(); let notification_handlers = notification_handlers.clone(); let response_handlers = response_handlers.clone(); let io_handlers = io_handlers.clone(); async move |cx| { - Self::handle_incoming_messages( + Self::handle_input( stdout, - unhandled_notification_wrapper, + on_unhandled_notification, notification_handlers, response_handlers, io_handlers, @@ -457,7 +433,7 @@ impl LanguageServer { stdout.or(stderr) }); let output_task = cx.background_spawn({ - Self::handle_outgoing_messages( + Self::handle_output( stdin, outbound_rx, output_done_tx, @@ -503,9 +479,9 @@ impl LanguageServer { self.code_action_kinds.clone() } - async fn handle_incoming_messages( + async fn handle_input( stdout: Stdout, - on_unhandled_notification: impl AsyncFn(NotificationOrRequest) + 'static + Send, + mut on_unhandled_notification: F, notification_handlers: Arc>>, response_handlers: Arc>>>, io_handlers: Arc>>, @@ -513,6 +489,7 @@ impl LanguageServer { ) -> anyhow::Result<()> where Stdout: AsyncRead + Unpin + Send + 'static, + F: FnMut(AnyNotification) + 'static + Send, { use smol::stream::StreamExt; let stdout = BufReader::new(stdout); @@ -529,19 +506,15 @@ impl LanguageServer { cx.background_executor().clone(), ); - while let Some(msg) = input_handler.incoming_messages.next().await { - let unhandled_message = { + while let Some(msg) = input_handler.notifications_channel.next().await { + { let mut notification_handlers = notification_handlers.lock(); if let Some(handler) = notification_handlers.get_mut(msg.method.as_str()) { handler(msg.id, msg.params.unwrap_or(Value::Null), cx); - None } else { - Some(msg) + drop(notification_handlers); + on_unhandled_notification(msg); } - }; - - if let Some(msg) = unhandled_message { - on_unhandled_notification(msg).await; } // Don't starve the main thread when receiving lots of notifications at once. @@ -585,7 +558,7 @@ impl LanguageServer { } } - async fn handle_outgoing_messages( + async fn handle_output( stdin: Stdin, outbound_rx: channel::Receiver, output_done_tx: barrier::Sender, @@ -651,7 +624,7 @@ impl LanguageServer { capabilities: ClientCapabilities { general: Some(GeneralClientCapabilities { position_encodings: Some(vec![PositionEncodingKind::UTF16]), - ..GeneralClientCapabilities::default() + ..Default::default() }), workspace: Some(WorkspaceClientCapabilities { configuration: Some(true), @@ -665,7 +638,6 @@ impl LanguageServer { workspace_folders: Some(true), symbol: Some(WorkspaceSymbolClientCapabilities { resolve_support: None, - dynamic_registration: Some(true), ..WorkspaceSymbolClientCapabilities::default() }), inlay_hint: Some(InlayHintWorkspaceClientCapabilities { @@ -689,21 +661,21 @@ impl LanguageServer { ..WorkspaceEditClientCapabilities::default() }), file_operations: Some(WorkspaceFileOperationsClientCapabilities { - dynamic_registration: Some(true), + dynamic_registration: Some(false), did_rename: Some(true), will_rename: Some(true), - ..WorkspaceFileOperationsClientCapabilities::default() + ..Default::default() }), apply_edit: Some(true), execute_command: Some(ExecuteCommandClientCapabilities { - dynamic_registration: Some(true), + dynamic_registration: Some(false), }), - ..WorkspaceClientCapabilities::default() + ..Default::default() }), text_document: Some(TextDocumentClientCapabilities { definition: Some(GotoCapability { link_support: Some(true), - dynamic_registration: Some(true), + dynamic_registration: None, }), code_action: Some(CodeActionClientCapabilities { code_action_literal_support: Some(CodeActionLiteralSupport { @@ -726,8 +698,7 @@ impl LanguageServer { "command".to_string(), ], }), - dynamic_registration: Some(true), - ..CodeActionClientCapabilities::default() + ..Default::default() }), completion: Some(CompletionClientCapabilities { completion_item: Some(CompletionItemCapability { @@ -749,11 +720,7 @@ impl LanguageServer { InsertTextMode::ADJUST_INDENTATION, ], }), - documentation_format: Some(vec![ - MarkupKind::Markdown, - MarkupKind::PlainText, - ]), - ..CompletionItemCapability::default() + ..Default::default() }), insert_text_mode: Some(InsertTextMode::ADJUST_INDENTATION), completion_list: Some(CompletionListCapability { @@ -766,20 +733,18 @@ impl LanguageServer { ]), }), context_support: Some(true), - dynamic_registration: Some(true), - ..CompletionClientCapabilities::default() + ..Default::default() }), rename: Some(RenameClientCapabilities { prepare_support: Some(true), prepare_support_default_behavior: Some( PrepareSupportDefaultBehavior::IDENTIFIER, ), - dynamic_registration: Some(true), - ..RenameClientCapabilities::default() + ..Default::default() }), hover: Some(HoverClientCapabilities { content_format: Some(vec![MarkupKind::Markdown]), - dynamic_registration: Some(true), + dynamic_registration: None, }), inlay_hint: Some(InlayHintClientCapabilities { resolve_support: Some(InlayHintResolveClientCapabilities { @@ -791,7 +756,7 @@ impl LanguageServer { "label.command".to_string(), ], }), - dynamic_registration: Some(true), + dynamic_registration: Some(false), }), publish_diagnostics: Some(PublishDiagnosticsClientCapabilities { related_information: Some(true), @@ -822,29 +787,26 @@ impl LanguageServer { }), active_parameter_support: Some(true), }), - dynamic_registration: Some(true), ..SignatureHelpClientCapabilities::default() }), synchronization: Some(TextDocumentSyncClientCapabilities { did_save: Some(true), - dynamic_registration: Some(true), ..TextDocumentSyncClientCapabilities::default() }), code_lens: Some(CodeLensClientCapabilities { - dynamic_registration: Some(true), + dynamic_registration: Some(false), }), document_symbol: Some(DocumentSymbolClientCapabilities { hierarchical_document_symbol_support: Some(true), - dynamic_registration: Some(true), ..DocumentSymbolClientCapabilities::default() }), diagnostic: Some(DiagnosticClientCapabilities { - dynamic_registration: Some(true), + dynamic_registration: Some(false), related_document_support: Some(true), }) .filter(|_| pull_diagnostics), color_provider: Some(DocumentColorClientCapabilities { - dynamic_registration: Some(true), + dynamic_registration: Some(false), }), ..TextDocumentClientCapabilities::default() }), @@ -857,7 +819,7 @@ impl LanguageServer { show_message: Some(ShowMessageRequestClientCapabilities { message_action_item: None, }), - ..WindowClientCapabilities::default() + ..Default::default() }), }, trace: None, @@ -869,7 +831,8 @@ impl LanguageServer { } }), locale: None, - ..InitializeParams::default() + + ..Default::default() } } @@ -1073,9 +1036,7 @@ impl LanguageServer { jsonrpc: JSON_RPC_VERSION, id, value: LspResult::Error(Some(Error { - code: lsp_types::error_codes::REQUEST_FAILED, message: error.to_string(), - data: None, })), }, }; @@ -1096,9 +1057,7 @@ impl LanguageServer { id, result: None, error: Some(Error { - code: -32700, // Parse error message: error.to_string(), - data: None, }), }; if let Some(response) = serde_json::to_string(&response).log_err() { @@ -1600,7 +1559,7 @@ impl FakeLanguageServer { root, Some(workspace_folders.clone()), cx, - |_| false, + |_| {}, ); server.process_name = process_name; let fake = FakeLanguageServer { @@ -1623,10 +1582,9 @@ impl FakeLanguageServer { notifications_tx .try_send(( msg.method.to_string(), - msg.params.as_ref().unwrap_or(&Value::Null).to_string(), + msg.params.unwrap_or(Value::Null).to_string(), )) .ok(); - true }, ); server.process_name = name.as_str().into(); @@ -1678,7 +1636,7 @@ impl LanguageServer { workspace_symbol_provider: Some(OneOf::Left(true)), implementation_provider: Some(ImplementationProviderCapability::Simple(true)), type_definition_provider: Some(TypeDefinitionProviderCapability::Simple(true)), - ..ServerCapabilities::default() + ..Default::default() } } } @@ -1904,7 +1862,7 @@ mod tests { #[gpui::test] fn test_deserialize_string_digit_id() { let json = r#"{"jsonrpc":"2.0","id":"2","method":"workspace/configuration","params":{"items":[{"scopeUri":"file:///Users/mph/Devel/personal/hello-scala/","section":"metals"}]}}"#; - let notification = serde_json::from_str::(json) + let notification = serde_json::from_str::(json) .expect("message with string id should be parsed"); let expected_id = RequestId::Str("2".to_string()); assert_eq!(notification.id, Some(expected_id)); @@ -1913,7 +1871,7 @@ mod tests { #[gpui::test] fn test_deserialize_string_id() { let json = r#"{"jsonrpc":"2.0","id":"anythingAtAll","method":"workspace/configuration","params":{"items":[{"scopeUri":"file:///Users/mph/Devel/personal/hello-scala/","section":"metals"}]}}"#; - let notification = serde_json::from_str::(json) + let notification = serde_json::from_str::(json) .expect("message with string id should be parsed"); let expected_id = RequestId::Str("anythingAtAll".to_string()); assert_eq!(notification.id, Some(expected_id)); @@ -1922,7 +1880,7 @@ mod tests { #[gpui::test] fn test_deserialize_int_id() { let json = r#"{"jsonrpc":"2.0","id":2,"method":"workspace/configuration","params":{"items":[{"scopeUri":"file:///Users/mph/Devel/personal/hello-scala/","section":"metals"}]}}"#; - let notification = serde_json::from_str::(json) + let notification = serde_json::from_str::(json) .expect("message with string id should be parsed"); let expected_id = RequestId::Int(2); assert_eq!(notification.id, Some(expected_id)); diff --git a/crates/markdown/examples/markdown.rs b/crates/markdown/examples/markdown.rs index c651c7921d..bf685bd9ac 100644 --- a/crates/markdown/examples/markdown.rs +++ b/crates/markdown/examples/markdown.rs @@ -77,16 +77,16 @@ impl Render for MarkdownExample { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let markdown_style = MarkdownStyle { base_text_style: gpui::TextStyle { - font_family: ".ZedSans".into(), + font_family: "Zed Plex Sans".into(), color: cx.theme().colors().terminal_ansi_black, ..Default::default() }, code_block: StyleRefinement::default() - .font_family(".ZedMono") + .font_family("Zed Plex Mono") .m(rems(1.)) .bg(rgb(0xAAAAAAA)), inline_code: gpui::TextStyleRefinement { - font_family: Some(".ZedMono".into()), + font_family: Some("Zed Mono".into()), color: Some(cx.theme().colors().editor_foreground), background_color: Some(cx.theme().colors().editor_background), ..Default::default() diff --git a/crates/markdown/examples/markdown_as_child.rs b/crates/markdown/examples/markdown_as_child.rs index 16c198601a..862b657c8c 100644 --- a/crates/markdown/examples/markdown_as_child.rs +++ b/crates/markdown/examples/markdown_as_child.rs @@ -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, node_runtime, cx); + languages::init(language_registry.clone(), node_runtime, cx); theme::init(LoadThemes::JustBase, cx); Assets.load_fonts(cx).unwrap(); diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index 1f607a033a..dba4bc64b1 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -340,26 +340,27 @@ impl Markdown { } for (range, event) in &events { - 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); + 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); + } + } } } } @@ -658,13 +659,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() - && 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() { + 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); + } } } } else if markdown.selection.pending { @@ -757,10 +758,10 @@ impl Element for MarkdownElement { let mut current_img_block_range: Option> = 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 - && current_img_block_range.end > range.end - { - continue; + if let Some(current_img_block_range) = ¤t_img_block_range { + if current_img_block_range.end > range.end { + continue; + } } match event { @@ -874,7 +875,7 @@ impl Element for MarkdownElement { (CodeBlockRenderer::Custom { render, .. }, _) => { let parent_container = render( kind, - parsed_markdown, + &parsed_markdown, range.clone(), metadata.clone(), window, @@ -1083,15 +1084,7 @@ impl Element for MarkdownElement { self.markdown.clone(), cx, ); - el.child( - h_flex() - .w_4() - .absolute() - .top_1p5() - .right_1p5() - .justify_end() - .child(codeblock), - ) + el.child(div().absolute().top_1().right_1().w_5().child(codeblock)) }); } @@ -1115,12 +1108,11 @@ impl Element for MarkdownElement { cx, ); el.child( - h_flex() - .w_4() + div() .absolute() .top_0() .right_0() - .justify_end() + .w_5() .visible_on_hover("code_block") .child(codeblock), ) @@ -1320,12 +1312,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")) + .tooltip(Tooltip::text("Copy Code")) .on_click({ - let markdown = markdown; + let id = id.clone(); + let markdown = markdown.clone(); move |_event, _window, cx| { let id = id.clone(); markdown.update(cx, |this, cx| { @@ -1702,10 +1693,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() - && position.y < next_line.layout.bounds().top() - { - return Err(line.source_end); + if let Some(next_line) = lines.peek() { + if position.y < next_line.layout.bounds().top() { + return Err(line.source_end); + } } continue; diff --git a/crates/markdown/src/parser.rs b/crates/markdown/src/parser.rs index 3720e5b1ef..1035335ccb 100644 --- a/crates/markdown/src/parser.rs +++ b/crates/markdown/src/parser.rs @@ -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; diff --git a/crates/markdown_preview/src/markdown_parser.rs b/crates/markdown_preview/src/markdown_parser.rs index b51b98a2ed..27691f2ecf 100644 --- a/crates/markdown_preview/src/markdown_parser.rs +++ b/crates/markdown_preview/src/markdown_parser.rs @@ -76,22 +76,22 @@ impl<'a> MarkdownParser<'a> { if self.eof() || (steps + self.cursor) >= self.tokens.len() { return self.tokens.last(); } - self.tokens.get(self.cursor + steps) + return self.tokens.get(self.cursor + steps); } fn previous(&self) -> Option<&(Event<'_>, Range)> { if self.cursor == 0 || self.cursor > self.tokens.len() { return None; } - self.tokens.get(self.cursor - 1) + return self.tokens.get(self.cursor - 1); } fn current(&self) -> Option<&(Event<'_>, Range)> { - self.peek(0) + return self.peek(0); } fn current_event(&self) -> Option<&Event<'_>> { - self.current().map(|(event, _)| event) + return self.current().map(|(event, _)| event); } fn is_text_like(event: &Event) -> bool { @@ -178,6 +178,7 @@ impl<'a> MarkdownParser<'a> { _ => None, }, Event::Rule => { + let source_range = source_range.clone(); self.cursor += 1; Some(vec![ParsedMarkdownElement::HorizontalRule(source_range)]) } @@ -299,12 +300,13 @@ 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() - && 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() { + if 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(( @@ -400,7 +402,7 @@ impl<'a> MarkdownParser<'a> { } if !text.is_empty() { markdown_text_like.push(MarkdownParagraphChunk::Text(ParsedMarkdownText { - source_range, + source_range: source_range.clone(), contents: text, highlights, regions, @@ -419,7 +421,7 @@ impl<'a> MarkdownParser<'a> { self.cursor += 1; ParsedMarkdownHeading { - source_range, + source_range: source_range.clone(), level: match level { pulldown_cmark::HeadingLevel::H1 => HeadingLevel::H1, pulldown_cmark::HeadingLevel::H2 => HeadingLevel::H2, @@ -577,10 +579,10 @@ impl<'a> MarkdownParser<'a> { } } else { let block = self.parse_block().await; - if let Some(block) = block - && let Some(list_item) = items_stack.last_mut() - { - list_item.content.extend(block); + if let Some(block) = block { + if let Some(list_item) = items_stack.last_mut() { + list_item.content.extend(block); + } } } } diff --git a/crates/markdown_preview/src/markdown_preview_view.rs b/crates/markdown_preview/src/markdown_preview_view.rs index 1121d64655..03cfd7ee82 100644 --- a/crates/markdown_preview/src/markdown_preview_view.rs +++ b/crates/markdown_preview/src/markdown_preview_view.rs @@ -18,7 +18,6 @@ use workspace::item::{Item, ItemHandle}; use workspace::{Pane, Workspace}; use crate::markdown_elements::ParsedMarkdownElement; -use crate::markdown_renderer::CheckboxClickedEvent; use crate::{ MovePageDown, MovePageUp, OpenFollowingPreview, OpenPreview, OpenPreviewToTheSide, markdown_elements::ParsedMarkdown, @@ -115,7 +114,8 @@ impl MarkdownPreviewView { pane.activate_item(existing_follow_view_idx, true, true, window, cx); }); } else { - let view = Self::create_following_markdown_view(workspace, editor, window, cx); + let view = + Self::create_following_markdown_view(workspace, editor.clone(), window, cx); workspace.active_pane().update(cx, |pane, cx| { pane.add_item(Box::new(view.clone()), true, true, None, window, cx) }); @@ -150,9 +150,10 @@ impl MarkdownPreviewView { if let Some(editor) = workspace .active_item(cx) .and_then(|item| item.act_as::(cx)) - && Self::is_markdown_file(&editor, cx) { - return Some(editor); + if Self::is_markdown_file(&editor, cx) { + return Some(editor); + } } None } @@ -202,7 +203,114 @@ impl MarkdownPreviewView { cx: &mut Context, ) -> Entity { cx.new(|cx| { - let list_state = ListState::new(0, gpui::ListAlignment::Top, px(1000.)); + let view = cx.entity().downgrade(); + + let list_state = ListState::new( + 0, + gpui::ListAlignment::Top, + px(1000.), + move |ix, window, cx| { + if let Some(view) = view.upgrade() { + view.update(cx, |this: &mut Self, cx| { + let Some(contents) = &this.contents else { + return div().into_any(); + }; + + let mut render_cx = + RenderContext::new(Some(this.workspace.clone()), window, cx) + .with_checkbox_clicked_callback({ + let view = view.clone(); + move |checked, source_range, window, cx| { + view.update(cx, |view, cx| { + if let Some(editor) = view + .active_editor + .as_ref() + .map(|s| s.editor.clone()) + { + editor.update(cx, |editor, cx| { + let task_marker = + if checked { "[x]" } else { "[ ]" }; + + editor.edit( + vec![(source_range, task_marker)], + cx, + ); + }); + view.parse_markdown_from_active_editor( + false, window, cx, + ); + cx.notify(); + } + }) + } + }); + + let block = contents.children.get(ix).unwrap(); + let rendered_block = render_markdown_block(block, &mut render_cx); + + let should_apply_padding = Self::should_apply_padding_between( + block, + contents.children.get(ix + 1), + ); + + div() + .id(ix) + .when(should_apply_padding, |this| { + this.pb(render_cx.scaled_rems(0.75)) + }) + .group("markdown-block") + .on_click(cx.listener( + move |this, event: &ClickEvent, window, cx| { + if event.down.click_count == 2 { + if let Some(source_range) = this + .contents + .as_ref() + .and_then(|c| c.children.get(ix)) + .and_then(|block| block.source_range()) + { + this.move_cursor_to_block( + window, + cx, + source_range.start..source_range.start, + ); + } + } + }, + )) + .map(move |container| { + let indicator = div() + .h_full() + .w(px(4.0)) + .when(ix == this.selected_block, |this| { + this.bg(cx.theme().colors().border) + }) + .group_hover("markdown-block", |s| { + if ix == this.selected_block { + s + } else { + s.bg(cx.theme().colors().border_variant) + } + }) + .rounded_xs(); + + container.child( + div() + .relative() + .child( + div() + .pl(render_cx.scaled_rems(1.0)) + .child(rendered_block), + ) + .child(indicator.absolute().left_0().top_0()), + ) + }) + .into_any() + }) + } else { + div().into_any() + } + }, + ); let mut this = Self { selected_block: 0, @@ -241,30 +349,32 @@ impl MarkdownPreviewView { window: &mut Window, cx: &mut Context, ) { - if let Some(item) = active_item - && item.item_id() != cx.entity_id() - && let Some(editor) = item.act_as::(cx) - && Self::is_markdown_file(&editor, cx) - { - self.set_editor(editor, window, cx); + if let Some(item) = active_item { + if item.item_id() != cx.entity_id() { + if let Some(editor) = item.act_as::(cx) { + if Self::is_markdown_file(&editor, cx) { + self.set_editor(editor, window, cx); + } + } + } } } pub fn is_markdown_file(editor: &Entity, cx: &mut Context) -> bool { let buffer = editor.read(cx).buffer().read(cx); - if let Some(buffer) = buffer.as_singleton() - && let Some(language) = buffer.read(cx).language() - { - return language.name() == "Markdown".into(); + if let Some(buffer) = buffer.as_singleton() { + if let Some(language) = buffer.read(cx).language() { + return language.name() == "Markdown".into(); + } } false } fn set_editor(&mut self, editor: Entity, window: &mut Window, cx: &mut Context) { - if let Some(active) = &self.active_editor - && active.editor == editor - { - return; + if let Some(active) = &self.active_editor { + if active.editor == editor { + return; + } } let subscription = cx.subscribe_in( @@ -497,106 +607,10 @@ impl Render for MarkdownPreviewView { .p_4() .text_size(buffer_size) .line_height(relative(buffer_line_height.value())) - .child(div().flex_grow().map(|this| { - this.child( - list( - self.list_state.clone(), - cx.processor(|this, ix, window, cx| { - let Some(contents) = &this.contents else { - return div().into_any(); - }; - - let mut render_cx = - RenderContext::new(Some(this.workspace.clone()), window, cx) - .with_checkbox_clicked_callback(cx.listener( - move |this, e: &CheckboxClickedEvent, window, cx| { - if let Some(editor) = this - .active_editor - .as_ref() - .map(|s| s.editor.clone()) - { - editor.update(cx, |editor, cx| { - let task_marker = - if e.checked() { "[x]" } else { "[ ]" }; - - editor.edit( - vec![(e.source_range(), task_marker)], - cx, - ); - }); - this.parse_markdown_from_active_editor( - false, window, cx, - ); - cx.notify(); - } - }, - )); - - let block = contents.children.get(ix).unwrap(); - let rendered_block = render_markdown_block(block, &mut render_cx); - - let should_apply_padding = Self::should_apply_padding_between( - block, - contents.children.get(ix + 1), - ); - - div() - .id(ix) - .when(should_apply_padding, |this| { - this.pb(render_cx.scaled_rems(0.75)) - }) - .group("markdown-block") - .on_click(cx.listener( - move |this, event: &ClickEvent, window, cx| { - 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, - ); - } - }, - )) - .map(move |container| { - let indicator = div() - .h_full() - .w(px(4.0)) - .when(ix == this.selected_block, |this| { - this.bg(cx.theme().colors().border) - }) - .group_hover("markdown-block", |s| { - if ix == this.selected_block { - s - } else { - s.bg(cx.theme().colors().border_variant) - } - }) - .rounded_xs(); - - container.child( - div() - .relative() - .child( - div() - .pl(render_cx.scaled_rems(1.0)) - .child(rendered_block), - ) - .child(indicator.absolute().left_0().top_0()), - ) - }) - .into_any() - }), - ) - .size_full(), - ) - })) + .child( + div() + .flex_grow() + .map(|this| this.child(list(self.list_state.clone()).size_full())), + ) } } diff --git a/crates/markdown_preview/src/markdown_renderer.rs b/crates/markdown_preview/src/markdown_renderer.rs index b0b10e927c..80bed8a6e8 100644 --- a/crates/markdown_preview/src/markdown_renderer.rs +++ b/crates/markdown_preview/src/markdown_renderer.rs @@ -26,22 +26,7 @@ use ui::{ }; use workspace::{OpenOptions, OpenVisible, Workspace}; -pub struct CheckboxClickedEvent { - pub checked: bool, - pub source_range: Range, -} - -impl CheckboxClickedEvent { - pub fn source_range(&self) -> Range { - self.source_range.clone() - } - - pub fn checked(&self) -> bool { - self.checked - } -} - -type CheckboxClickedCallback = Arc>; +type CheckboxClickedCallback = Arc, &mut Window, &mut App)>>; #[derive(Clone)] pub struct RenderContext { @@ -95,7 +80,7 @@ impl RenderContext { pub fn with_checkbox_clicked_callback( mut self, - callback: impl Fn(&CheckboxClickedEvent, &mut Window, &mut App) + 'static, + callback: impl Fn(bool, Range, &mut Window, &mut App) + 'static, ) -> Self { self.checkbox_clicked_callback = Some(Arc::new(Box::new(callback))); self @@ -111,10 +96,11 @@ 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 { - self.buffer_text_style + return self + .buffer_text_style .font_size .to_rems(self.window_rem_size) - .mul(rems) + .mul(rems); } /// This ensures that children inside of block quotes @@ -243,14 +229,7 @@ fn render_markdown_list_item( }; if window.modifiers().secondary() { - callback( - &CheckboxClickedEvent { - checked, - source_range: range.clone(), - }, - window, - cx, - ); + callback(checked, range.clone(), window, cx); } } }) @@ -458,13 +437,13 @@ fn render_markdown_table(parsed: &ParsedMarkdownTable, cx: &mut RenderContext) - let mut max_lengths: Vec = 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; diff --git a/crates/migrator/src/migrations/m_2025_01_02/settings.rs b/crates/migrator/src/migrations/m_2025_01_02/settings.rs index a35b1ebd2e..3ce85e6b26 100644 --- a/crates/migrator/src/migrations/m_2025_01_02/settings.rs +++ b/crates/migrator/src/migrations/m_2025_01_02/settings.rs @@ -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)?; + let parent_object_name = contents.get(parent_object_range.clone())?; 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)?; + let setting_name = contents.get(setting_name_range.clone())?; let setting_value_ix = query.capture_index_for_name("setting_value")?; let setting_value_range = mat diff --git a/crates/migrator/src/migrations/m_2025_01_29/keymap.rs b/crates/migrator/src/migrations/m_2025_01_29/keymap.rs index eed2c46e08..c32da88229 100644 --- a/crates/migrator/src/migrations/m_2025_01_29/keymap.rs +++ b/crates/migrator/src/migrations/m_2025_01_29/keymap.rs @@ -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)) + Some((context_predicate_range, new_predicate.to_string())) } else { None } diff --git a/crates/migrator/src/migrations/m_2025_01_29/settings.rs b/crates/migrator/src/migrations/m_2025_01_29/settings.rs index 46cfe2f178..8d3261676b 100644 --- a/crates/migrator/src/migrations/m_2025_01_29/settings.rs +++ b/crates/migrator/src/migrations/m_2025_01_29/settings.rs @@ -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)?; + let parent_object_name = contents.get(parent_object_range.clone())?; let setting_name_ix = query.capture_index_for_name("setting_name")?; let setting_range = mat diff --git a/crates/migrator/src/migrations/m_2025_01_30/settings.rs b/crates/migrator/src/migrations/m_2025_01_30/settings.rs index 2d763e4722..23a3243b82 100644 --- a/crates/migrator/src/migrations/m_2025_01_30/settings.rs +++ b/crates/migrator/src/migrations/m_2025_01_30/settings.rs @@ -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)?; + let parent_object_name = contents.get(parent_object_range.clone())?; 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)?; + let parent_object_name = contents.get(parent_object_range.clone())?; 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)?; + let setting_name = contents.get(setting_name_range.clone())?; let setting_value_ix = query.capture_index_for_name("setting_value")?; let setting_value_range = mat diff --git a/crates/migrator/src/migrations/m_2025_03_29/settings.rs b/crates/migrator/src/migrations/m_2025_03_29/settings.rs index 8f83d8e39e..47f65b407d 100644 --- a/crates/migrator/src/migrations/m_2025_03_29/settings.rs +++ b/crates/migrator/src/migrations/m_2025_03_29/settings.rs @@ -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)?; + let setting_name = contents.get(setting_name_range.clone())?; if setting_name != "hide_mouse_while_typing" { return None; diff --git a/crates/migrator/src/migrations/m_2025_05_05/settings.rs b/crates/migrator/src/migrations/m_2025_05_05/settings.rs index 77da1b9a07..88c6c338d1 100644 --- a/crates/migrator/src/migrations/m_2025_05_05/settings.rs +++ b/crates/migrator/src/migrations/m_2025_05_05/settings.rs @@ -24,7 +24,7 @@ fn rename_assistant( .nodes_for_capture_index(key_capture_ix) .next()? .byte_range(); - Some((key_range, "agent".to_string())) + return 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(); - Some((key_range, "enabled_in_text_threads".to_string())) + return Some((key_range, "enabled_in_text_threads".to_string())); } diff --git a/crates/migrator/src/migrations/m_2025_05_29/settings.rs b/crates/migrator/src/migrations/m_2025_05_29/settings.rs index 37ef0e45cc..56d72836fa 100644 --- a/crates/migrator/src/migrations/m_2025_05_29/settings.rs +++ b/crates/migrator/src/migrations/m_2025_05_29/settings.rs @@ -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)?; + let parent_object_name = contents.get(parent_object_range.clone())?; 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)?; + let setting_name = contents.get(setting_name_range.clone())?; if setting_name != "preferred_completion_mode" { return None; diff --git a/crates/migrator/src/migrations/m_2025_06_16/settings.rs b/crates/migrator/src/migrations/m_2025_06_16/settings.rs index cd79eae204..cce407e21b 100644 --- a/crates/migrator/src/migrations/m_2025_06_16/settings.rs +++ b/crates/migrator/src/migrations/m_2025_06_16/settings.rs @@ -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" - && 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" { + 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, + } } } } diff --git a/crates/migrator/src/migrations/m_2025_06_25/settings.rs b/crates/migrator/src/migrations/m_2025_06_25/settings.rs index 2bf7658eeb..5dd6c3093a 100644 --- a/crates/migrator/src/migrations/m_2025_06_25/settings.rs +++ b/crates/migrator/src/migrations/m_2025_06_25/settings.rs @@ -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() - && prev_sibling.kind() == "," - { - range_to_remove.start = prev_sibling.start_byte(); + if let Some(prev_sibling) = pair_node.prev_sibling() { + if 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') - && 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') { + if text_after[..newline_pos].chars().all(|c| c.is_whitespace()) { + range_to_remove.end += newline_pos + 1; + } } Some((range_to_remove, String::new())) diff --git a/crates/migrator/src/migrations/m_2025_06_27/settings.rs b/crates/migrator/src/migrations/m_2025_06_27/settings.rs index e3e951b1a6..6156308fce 100644 --- a/crates/migrator/src/migrations/m_2025_06_27/settings.rs +++ b/crates/migrator/src/migrations/m_2025_06_27/settings.rs @@ -56,18 +56,19 @@ fn flatten_context_server_command( let mut cursor = command_object.walk(); for child in command_object.children(&mut cursor) { - 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]), - _ => {} + 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]), + _ => {} + } + } } } } diff --git a/crates/migrator/src/migrator.rs b/crates/migrator/src/migrator.rs index 2180a049d0..b425f7f1d5 100644 --- a/crates/migrator/src/migrator.rs +++ b/crates/migrator/src/migrator.rs @@ -28,7 +28,7 @@ fn migrate(text: &str, patterns: MigrationPatterns, query: &Query) -> Result Result Result> { pub fn migrate_edit_prediction_provider_settings(text: &str) -> Result> { 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); } diff --git a/crates/mistral/src/mistral.rs b/crates/mistral/src/mistral.rs index 5b4d05377c..c466a598a0 100644 --- a/crates/mistral/src/mistral.rs +++ b/crates/mistral/src/mistral.rs @@ -86,7 +86,6 @@ pub enum Model { max_completion_tokens: Option, supports_tools: Option, supports_images: Option, - supports_thinking: Option, }, } @@ -215,16 +214,6 @@ impl Model { } => supports_images.unwrap_or(false), } } - - pub fn supports_thinking(&self) -> bool { - match self { - Self::MagistralMediumLatest | Self::MagistralSmallLatest => true, - Self::Custom { - supports_thinking, .. - } => supports_thinking.unwrap_or(false), - _ => false, - } - } } #[derive(Debug, Serialize, Deserialize)] @@ -299,9 +288,7 @@ pub enum ToolChoice { #[serde(tag = "role", rename_all = "lowercase")] pub enum RequestMessage { Assistant { - #[serde(flatten)] - #[serde(default, skip_serializing_if = "Option::is_none")] - content: Option, + content: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] tool_calls: Vec, }, @@ -310,8 +297,7 @@ pub enum RequestMessage { content: MessageContent, }, System { - #[serde(flatten)] - content: MessageContent, + content: String, }, Tool { content: String, @@ -319,7 +305,7 @@ pub enum RequestMessage { }, } -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] #[serde(untagged)] pub enum MessageContent { #[serde(rename = "content")] @@ -360,21 +346,11 @@ impl MessageContent { } } -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] #[serde(tag = "type", rename_all = "snake_case")] pub enum MessagePart { Text { text: String }, ImageUrl { image_url: String }, - Thinking { thinking: Vec }, -} - -// Backwards-compatibility alias for provider code that refers to ContentPart -pub type ContentPart = MessagePart; - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum ThinkingPart { - Text { text: String }, } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] @@ -442,30 +418,24 @@ pub struct StreamChoice { pub finish_reason: Option, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug)] pub struct StreamDelta { pub role: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub content: Option, + pub content: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub tool_calls: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub reasoning_content: Option, } -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] -#[serde(untagged)] -pub enum MessageContentDelta { - Text(String), - Parts(Vec), -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] pub struct ToolCallChunk { pub index: usize, pub id: Option, pub function: Option, } -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] pub struct FunctionChunk { pub name: Option, pub arguments: Option, diff --git a/crates/multi_buffer/src/anchor.rs b/crates/multi_buffer/src/anchor.rs index 6bed0a4028..1305328d38 100644 --- a/crates/multi_buffer/src/anchor.rs +++ b/crates/multi_buffer/src/anchor.rs @@ -76,26 +76,27 @@ impl Anchor { if text_cmp.is_ne() { return text_cmp; } - if (self.diff_base_anchor.is_some() || other.diff_base_anchor.is_some()) - && let Some(base_text) = snapshot + if self.diff_base_anchor.is_some() || other.diff_base_anchor.is_some() { + if 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 @@ -106,49 +107,51 @@ impl Anchor { } pub fn bias_left(&self, snapshot: &MultiBufferSnapshot) -> Anchor { - 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 - }), - }; + 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 + }), + }; + } } *self } pub fn bias_right(&self, snapshot: &MultiBufferSnapshot) -> Anchor { - 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 - }), - }; + 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 + }), + }; + } } *self } @@ -209,7 +212,7 @@ impl AnchorRangeExt for Range { } fn includes(&self, other: &Range, 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, buffer: &MultiBufferSnapshot) -> bool { diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index e27cbf868a..f0913e30fb 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -43,7 +43,7 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; -use sum_tree::{Bias, Cursor, Dimension, Dimensions, SumTree, Summary, TreeMap}; +use sum_tree::{Bias, Cursor, Dimension, SumTree, Summary, TreeMap}; use text::{ BufferId, Edit, LineIndent, TextSummary, locator::Locator, @@ -474,7 +474,7 @@ pub struct MultiBufferRows<'a> { pub struct MultiBufferChunks<'a> { excerpts: Cursor<'a, Excerpt, ExcerptOffset>, - diff_transforms: Cursor<'a, DiffTransform, Dimensions>, + diff_transforms: Cursor<'a, DiffTransform, (usize, ExcerptOffset)>, diffs: &'a TreeMap, diff_base_chunks: Option<(BufferId, BufferChunks<'a>)>, buffer_chunk: Option>, @@ -835,7 +835,7 @@ impl MultiBuffer { this.convert_edits_to_buffer_edits(edits, &snapshot, &original_indent_columns); drop(snapshot); - let mut buffer_ids = Vec::with_capacity(buffer_edits.len()); + let mut buffer_ids = Vec::new(); for (buffer_id, mut edits) in buffer_edits { buffer_ids.push(buffer_id); edits.sort_by_key(|edit| edit.range.start); @@ -1082,11 +1082,11 @@ impl MultiBuffer { let mut ranges: Vec> = Vec::new(); for edit in edits { - 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; + 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; + } } ranges.push(edit.range); } @@ -1146,13 +1146,13 @@ impl MultiBuffer { pub fn last_transaction_id(&self, cx: &App) -> Option { if let Some(buffer) = self.as_singleton() { - buffer + return 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()?; - Some(last_transaction.id) + return Some(last_transaction.id); } } @@ -1212,24 +1212,25 @@ impl MultiBuffer { for range in buffer.edited_ranges_for_transaction_id::(*buffer_transaction) { for excerpt_id in &buffer_state.excerpts { cursor.seek(excerpt_id, Bias::Left); - if let Some(excerpt) = cursor.item() - && excerpt.locator == *excerpt_id - { - let excerpt_buffer_start = excerpt.range.context.start.summary::(buffer); - let excerpt_buffer_end = excerpt.range.context.end.summary::(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() { + if excerpt.locator == *excerpt_id { + let excerpt_buffer_start = + excerpt.range.context.start.summary::(buffer); + let excerpt_buffer_end = excerpt.range.context.end.summary::(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; + } } } } @@ -1250,25 +1251,25 @@ impl MultiBuffer { buffer.update(cx, |buffer, _| { buffer.merge_transactions(transaction, destination) }); - } 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 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 { - destination - .buffer_transactions - .insert(buffer_id, buffer_transaction_id); } } } @@ -1561,11 +1562,11 @@ impl MultiBuffer { }); let mut merged_ranges: Vec> = Vec::new(); for range in expanded_ranges { - if let Some(last_range) = merged_ranges.last_mut() - && last_range.context.end >= range.context.start - { - last_range.context.end = range.context.end; - continue; + 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; + } } merged_ranges.push(range) } @@ -1685,7 +1686,7 @@ impl MultiBuffer { cx: &mut Context, ) -> (Vec>, 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(); @@ -1725,7 +1726,7 @@ impl MultiBuffer { merged_ranges.push(range.clone()); counts.push(1); } - (merged_ranges, counts) + return (merged_ranges, counts); } fn update_path_excerpts( @@ -1783,7 +1784,7 @@ impl MultiBuffer { } Some(( *existing_id, - excerpt.range.context.to_point(buffer_snapshot), + excerpt.range.context.to_point(&buffer_snapshot), )) } else { None @@ -1793,25 +1794,25 @@ impl MultiBuffer { }; if let Some((last_id, last)) = to_insert.last_mut() { - 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(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((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; + 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; + } } } @@ -2104,10 +2105,10 @@ impl MultiBuffer { .flatten() { cursor.seek_forward(&Some(locator), Bias::Left); - if let Some(excerpt) = cursor.item() - && excerpt.locator == *locator - { - excerpts.push((excerpt.id, excerpt.range.clone())); + if let Some(excerpt) = cursor.item() { + if excerpt.locator == *locator { + excerpts.push((excerpt.id, excerpt.range.clone())); + } } } @@ -2119,10 +2120,10 @@ impl MultiBuffer { let buffers = self.buffers.borrow(); let mut excerpts = snapshot .excerpts - .cursor::, ExcerptDimension>>(&()); + .cursor::<(Option<&Locator>, ExcerptDimension)>(&()); let mut diff_transforms = snapshot .diff_transforms - .cursor::, OutputDimension>>(&()); + .cursor::<(ExcerptDimension, OutputDimension)>(&()); diff_transforms.next(); let locators = buffers .get(&buffer_id) @@ -2131,21 +2132,22 @@ impl MultiBuffer { let mut result = Vec::new(); for locator in locators { excerpts.seek_forward(&Some(locator), Bias::Left); - 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); + 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); - 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 @@ -2196,15 +2198,6 @@ impl MultiBuffer { }) } - pub fn buffer_for_anchor(&self, anchor: Anchor, cx: &App) -> Option> { - 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( &self, @@ -2288,7 +2281,7 @@ impl MultiBuffer { let mut new_excerpts = SumTree::default(); let mut cursor = snapshot .excerpts - .cursor::, ExcerptOffset>>(&()); + .cursor::<(Option<&Locator>, ExcerptOffset)>(&()); let mut edits = Vec::new(); let mut excerpt_ids = ids.iter().copied().peekable(); let mut removed_buffer_ids = Vec::new(); @@ -2323,12 +2316,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() - && next_excerpt.locator == *next_locator - { - excerpt_ids.next(); - excerpt = next_excerpt; - continue 'remove_excerpts; + if let Some(next_excerpt) = cursor.item() { + if next_excerpt.locator == *next_locator { + excerpt_ids.next(); + excerpt = next_excerpt; + continue 'remove_excerpts; + } } } @@ -2436,7 +2429,7 @@ impl MultiBuffer { cx.emit(match event { language::BufferEvent::Edited => Event::Edited { singleton_buffer_edited: true, - edited_buffer: Some(buffer), + edited_buffer: Some(buffer.clone()), }, language::BufferEvent::DirtyChanged => Event::DirtyChanged, language::BufferEvent::Saved => Event::Saved, @@ -2491,7 +2484,7 @@ impl MultiBuffer { let base_text_changed = snapshot .diffs .get(&buffer_id) - .is_none_or(|old_diff| !new_diff.base_texts_eq(old_diff)); + .map_or(true, |old_diff| !new_diff.base_texts_eq(old_diff)); snapshot.diffs.insert(buffer_id, new_diff); @@ -2499,35 +2492,35 @@ impl MultiBuffer { for locator in &buffer_state.excerpts { let mut cursor = snapshot .excerpts - .cursor::, ExcerptOffset>>(&()); + .cursor::<(Option<&Locator>, ExcerptOffset)>(&()); cursor.seek_forward(&Some(locator), Bias::Left); - 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; + 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, + }); } - 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, - }); } } @@ -2785,7 +2778,7 @@ impl MultiBuffer { if diff_hunk.excerpt_id.cmp(&end_excerpt_id, &snapshot).is_gt() { continue; } - if last_hunk_row.is_some_and(|row| row >= diff_hunk.row_range.start) { + if last_hunk_row.map_or(false, |row| row >= diff_hunk.row_range.start) { continue; } let start = Anchor::in_buffer( @@ -2852,7 +2845,7 @@ impl MultiBuffer { let mut new_excerpts = SumTree::default(); let mut cursor = snapshot .excerpts - .cursor::, ExcerptOffset>>(&()); + .cursor::<(Option<&Locator>, ExcerptOffset)>(&()); let mut edits = Vec::>::new(); let prefix = cursor.slice(&Some(locator), Bias::Left); @@ -2928,7 +2921,7 @@ impl MultiBuffer { let mut new_excerpts = SumTree::default(); let mut cursor = snapshot .excerpts - .cursor::, ExcerptOffset>>(&()); + .cursor::<(Option<&Locator>, ExcerptOffset)>(&()); let mut edits = Vec::>::new(); for locator in &locators { @@ -3049,7 +3042,7 @@ impl MultiBuffer { is_dirty |= buffer.is_dirty(); has_deleted_file |= buffer .file() - .is_some_and(|file| file.disk_state() == DiskState::Deleted); + .map_or(false, |file| file.disk_state() == DiskState::Deleted); has_conflict |= buffer.has_conflict(); } if edited { @@ -3063,7 +3056,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)); } } @@ -3074,7 +3067,7 @@ impl MultiBuffer { let mut new_excerpts = SumTree::default(); let mut cursor = snapshot .excerpts - .cursor::, ExcerptOffset>>(&()); + .cursor::<(Option<&Locator>, ExcerptOffset)>(&()); for (locator, buffer, buffer_edited) in excerpts_to_edit { new_excerpts.append(cursor.slice(&Some(locator), Bias::Left), &()); @@ -3142,7 +3135,7 @@ impl MultiBuffer { let mut excerpts = snapshot.excerpts.cursor::(&()); let mut old_diff_transforms = snapshot .diff_transforms - .cursor::>(&()); + .cursor::<(ExcerptOffset, usize)>(&()); let mut new_diff_transforms = SumTree::default(); let mut old_expanded_hunks = HashSet::default(); let mut output_edits = Vec::new(); @@ -3162,12 +3155,13 @@ 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() - && 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() { + 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(); + } } } @@ -3183,7 +3177,7 @@ impl MultiBuffer { &mut new_diff_transforms, &mut end_of_current_insert, &mut old_expanded_hunks, - snapshot, + &snapshot, change_kind, ); @@ -3207,10 +3201,9 @@ 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() - .is_none_or(|next_edit| next_edit.old.start >= old_diff_transforms.end().0) - { + if excerpt_edits.peek().map_or(true, |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 { @@ -3230,7 +3223,7 @@ impl MultiBuffer { old_expanded_hunks.clear(); self.push_buffer_content_transform( - snapshot, + &snapshot, &mut new_diff_transforms, excerpt_offset, end_of_current_insert, @@ -3267,7 +3260,7 @@ impl MultiBuffer { &self, edit: &Edit>, excerpts: &mut Cursor>, - old_diff_transforms: &mut Cursor, usize>>, + old_diff_transforms: &mut Cursor, usize)>, new_diff_transforms: &mut SumTree, end_of_current_insert: &mut Option<(TypedOffset, DiffTransformHunkInfo)>, old_expanded_hunks: &mut HashSet, @@ -3438,17 +3431,18 @@ impl MultiBuffer { inserted_hunk_info, summary, }) = subtree.first() - && self.extend_last_buffer_content_transform( + { + if 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, &()); } @@ -3462,13 +3456,14 @@ impl MultiBuffer { inserted_hunk_info: inserted_hunk_anchor, summary, } = transform - && self.extend_last_buffer_content_transform( + { + if self.extend_last_buffer_content_transform( new_transforms, inserted_hunk_anchor, summary, - ) - { - return; + ) { + return; + } } new_transforms.push(transform, &()); } @@ -3523,10 +3518,11 @@ impl MultiBuffer { summary, inserted_hunk_info: inserted_hunk_anchor, } = last_transform - && *inserted_hunk_anchor == new_inserted_hunk_info { - *summary += summary_to_add; - did_extend = true; + if *inserted_hunk_anchor == new_inserted_hunk_info { + *summary += summary_to_add; + did_extend = true; + } } }, &(), @@ -3569,7 +3565,9 @@ 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(ExcerptRange::new); + let excerpt_ranges = ranges + .into_iter() + .map(|range| ExcerptRange::new(range.clone())); multi.update(cx, |multi, cx| { multi.push_excerpts(buffer, excerpt_ranges, cx) }); @@ -3603,7 +3601,7 @@ impl MultiBuffer { let mut edits: Vec<(Range, Arc)> = Vec::new(); let mut last_end = None; for _ in 0..edit_count { - if last_end.is_some_and(|last_end| last_end >= snapshot.len()) { + if last_end.map_or(false, |last_end| last_end >= snapshot.len()) { break; } @@ -3918,8 +3916,8 @@ impl MultiBufferSnapshot { &self, range: Range, ) -> Vec<(&BufferSnapshot, Range, 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::(); cursor.seek(&start); @@ -3957,8 +3955,8 @@ impl MultiBufferSnapshot { &self, range: Range, ) -> impl Iterator, ExcerptId, Option)> + '_ { - 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::(); cursor.seek(&start); @@ -4039,10 +4037,10 @@ impl MultiBufferSnapshot { cursor.seek(&query_range.start); - if let Some(region) = cursor.region().filter(|region| !region.is_main_buffer) - && region.range.start > D::zero(&()) - { - cursor.prev() + if let Some(region) = cursor.region().filter(|region| !region.is_main_buffer) { + if region.range.start > D::zero(&()) { + cursor.prev() + } } iter::from_fn(move || { @@ -4072,15 +4070,19 @@ impl MultiBufferSnapshot { buffer_start = cursor.main_buffer_position()?; }; let mut buffer_end = excerpt.range.context.end.summary::(&excerpt.buffer); - if let Some((end_excerpt_id, end_buffer_offset)) = range_end - && excerpt.id == end_excerpt_id - { - buffer_end = buffer_end.min(end_buffer_offset); + 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); + } } - get_buffer_metadata(&excerpt.buffer, buffer_start..buffer_end).map(|iterator| { - &mut current_excerpt_metadata.insert((excerpt.id, iterator)).1 - }) + if let Some(iterator) = + get_buffer_metadata(&excerpt.buffer, buffer_start..buffer_end) + { + Some(&mut current_excerpt_metadata.insert((excerpt.id, iterator)).1) + } else { + None + } }; // Visit each metadata item. @@ -4142,10 +4144,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 - && excerpt.id == end_excerpt_id - { - return None; + if let Some((end_excerpt_id, _)) = range_end { + if excerpt.id == end_excerpt_id { + return None; + } } cursor.next_excerpt(); } @@ -4184,7 +4186,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)); } } @@ -4202,7 +4204,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)); } } @@ -4453,7 +4455,7 @@ impl MultiBufferSnapshot { let mut buffer_position = region.buffer_range.start; buffer_position.add_assign(&overshoot); let clipped_buffer_position = - clip_buffer_position(region.buffer, buffer_position, bias); + clip_buffer_position(®ion.buffer, buffer_position, bias); let mut position = region.range.start; position.add_assign(&(clipped_buffer_position - region.buffer_range.start)); position @@ -4483,7 +4485,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(region.buffer, buffer_key); + let buffer_value = convert_buffer_dimension(®ion.buffer, buffer_key); let mut result = start_value; result.add_assign(&(buffer_value - buffer_start_value)); result @@ -4620,20 +4622,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::(); - 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::() - .as_str() - == **delimiter - { - indent.push_str(delimiter); - break; + 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::() + .as_str() + == **delimiter + { + indent.push_str(&delimiter); + break; + } } } } @@ -4653,7 +4655,7 @@ impl MultiBufferSnapshot { return true; } } - true + return true; } pub fn prev_non_blank_row(&self, mut row: MultiBufferRow) -> Option { @@ -4711,9 +4713,7 @@ impl MultiBufferSnapshot { O: ToOffset, { let range = range.start.to_offset(self)..range.end.to_offset(self); - let mut cursor = self - .diff_transforms - .cursor::>(&()); + let mut cursor = self.diff_transforms.cursor::<(usize, ExcerptOffset)>(&()); cursor.seek(&range.start, Bias::Right); let Some(first_transform) = cursor.item() else { @@ -4867,10 +4867,7 @@ impl MultiBufferSnapshot { &self, anchor: &Anchor, excerpt_position: D, - diff_transforms: &mut Cursor< - DiffTransform, - Dimensions, OutputDimension>, - >, + diff_transforms: &mut Cursor, OutputDimension)>, ) -> D where D: TextDimension + Ord + Sub, @@ -4891,22 +4888,25 @@ impl MultiBufferSnapshot { base_text_byte_range, .. }) => { - if let Some(diff_base_anchor) = &anchor.diff_base_anchor - && let Some(base_text) = + if let Some(diff_base_anchor) = &anchor.diff_base_anchor { + if 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 { - let position_in_hunk = base_text.text_summary_for_range::( - base_text_byte_range.start..base_text_offset, - ); - position.add_assign(&position_in_hunk); - } else if at_transform_end { - diff_transforms.next(); - continue; + 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::( + base_text_byte_range.start..base_text_offset, + ); + position.add_assign(&position_in_hunk); + } else if at_transform_end { + diff_transforms.next(); + continue; + } + } } } } @@ -4927,7 +4927,7 @@ impl MultiBufferSnapshot { fn excerpt_offset_for_anchor(&self, anchor: &Anchor) -> ExcerptOffset { let mut cursor = self .excerpts - .cursor::, ExcerptOffset>>(&()); + .cursor::<(Option<&Locator>, ExcerptOffset)>(&()); let locator = self.excerpt_locator_for_id(anchor.excerpt_id); cursor.seek(&Some(locator), Bias::Left); @@ -4936,19 +4936,20 @@ impl MultiBufferSnapshot { } let mut position = cursor.start().1; - 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; + 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; + } } } position @@ -4958,7 +4959,7 @@ impl MultiBufferSnapshot { while let Some(replacement) = self.replaced_excerpts.get(&excerpt_id) { excerpt_id = *replacement; } - excerpt_id + return excerpt_id; } pub fn summaries_for_anchors<'a, D, I>(&'a self, anchors: I) -> Vec @@ -4970,7 +4971,7 @@ impl MultiBufferSnapshot { let mut cursor = self.excerpts.cursor::(&()); let mut diff_transforms_cursor = self .diff_transforms - .cursor::, OutputDimension>>(&()); + .cursor::<(ExcerptDimension, OutputDimension)>(&()); diff_transforms_cursor.next(); let mut summaries = Vec::new(); @@ -5076,9 +5077,9 @@ impl MultiBufferSnapshot { if point == region.range.end.key && region.has_trailing_newline { position.add_assign(&D::from_text_summary(&TextSummary::newline())); } - Some(position) + return Some(position); } else { - Some(D::from_text_summary(&self.text_summary())) + return Some(D::from_text_summary(&self.text_summary())); } }) } @@ -5118,7 +5119,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 - .is_some_and(|e| e.id == old_excerpt_id && e.contains(&anchor)) + .map_or(false, |e| e.id == old_excerpt_id && e.contains(&anchor)) || old_excerpt_id == ExcerptId::max() || old_excerpt_id == ExcerptId::min(); @@ -5200,17 +5201,18 @@ impl MultiBufferSnapshot { // Find the given position in the diff transforms. Determine the corresponding // offset in the excerpts, and whether the position is within a deleted hunk. - let mut diff_transforms = self - .diff_transforms - .cursor::>(&()); + let mut diff_transforms = self.diff_transforms.cursor::<(usize, ExcerptOffset)>(&()); diff_transforms.seek(&offset, Bias::Right); - 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(); + 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(); + } + _ => {} + } + } } let offset_in_transform = offset - diff_transforms.start().0; let mut excerpt_offset = diff_transforms.start().1; @@ -5237,9 +5239,18 @@ impl MultiBufferSnapshot { excerpt_offset += ExcerptOffset::new(offset_in_transform); }; + if let Some((excerpt_id, buffer_id, buffer)) = self.as_singleton() { + return Anchor { + buffer_id: Some(buffer_id), + excerpt_id: *excerpt_id, + text_anchor: buffer.anchor_at(excerpt_offset.value, bias), + diff_base_anchor, + }; + } + let mut excerpts = self .excerpts - .cursor::>>(&()); + .cursor::<(ExcerptOffset, Option)>(&()); excerpts.seek(&excerpt_offset, Bias::Right); if excerpts.item().is_none() && excerpt_offset == excerpts.start().0 && bias == Bias::Left { excerpts.prev(); @@ -5260,17 +5271,10 @@ impl MultiBufferSnapshot { text_anchor, diff_base_anchor, } + } else if excerpt_offset.is_zero() && bias == Bias::Left { + Anchor::min() } else { - let mut anchor = if excerpt_offset.is_zero() && bias == Bias::Left { - Anchor::min() - } else { - Anchor::max() - }; - // TODO this is a hack, remove it - if let Some((excerpt_id, _, _)) = self.as_singleton() { - anchor.excerpt_id = *excerpt_id; - } - anchor + Anchor::max() } } @@ -5285,17 +5289,17 @@ impl MultiBufferSnapshot { let locator = self.excerpt_locator_for_id(excerpt_id); let mut cursor = self.excerpts.cursor::>(&()); cursor.seek(locator, Bias::Left); - if let Some(excerpt) = cursor.item() - && excerpt.id == excerpt_id - { - let text_anchor = excerpt.clip_anchor(text_anchor); - drop(cursor); - return Some(Anchor { - buffer_id: Some(excerpt.buffer_id), - excerpt_id, - text_anchor, - diff_base_anchor: None, - }); + if let Some(excerpt) = cursor.item() { + if excerpt.id == excerpt_id { + let text_anchor = excerpt.clip_anchor(text_anchor); + drop(cursor); + return Some(Anchor { + buffer_id: Some(excerpt.buffer_id), + excerpt_id, + text_anchor, + diff_base_anchor: None, + }); + } } None } @@ -5337,7 +5341,7 @@ impl MultiBufferSnapshot { let start_locator = self.excerpt_locator_for_id(id); let mut excerpts = self .excerpts - .cursor::, ExcerptDimension>>(&()); + .cursor::<(Option<&Locator>, ExcerptDimension)>(&()); excerpts.seek(&Some(start_locator), Bias::Left); excerpts.prev(); @@ -5480,7 +5484,7 @@ impl MultiBufferSnapshot { let range_filter = |open: Range, close: Range| -> bool { excerpt_buffer_range.contains(&open.start) && excerpt_buffer_range.contains(&close.end) - && range_filter.is_none_or(|filter| filter(buffer, open, close)) + && range_filter.map_or(true, |filter| filter(buffer, open, close)) }; let (open, close) = excerpt.buffer().innermost_enclosing_bracket_ranges( @@ -5640,10 +5644,10 @@ impl MultiBufferSnapshot { .buffer .line_indents_in_row_range(buffer_start_row..buffer_end_row); cursor.next(); - Some(line_indents.map(move |(buffer_row, indent)| { + return Some(line_indents.map(move |(buffer_row, indent)| { let row = region.range.start.row + (buffer_row - region.buffer_range.start.row); (MultiBufferRow(row), indent, ®ion.excerpt.buffer) - })) + })); }) .flatten() } @@ -5680,10 +5684,10 @@ impl MultiBufferSnapshot { .buffer .reversed_line_indents_in_row_range(buffer_start_row..buffer_end_row); cursor.prev(); - Some(line_indents.map(move |(buffer_row, indent)| { + return Some(line_indents.map(move |(buffer_row, indent)| { let row = region.range.start.row + (buffer_row - region.buffer_range.start.row); (MultiBufferRow(row), indent, ®ion.excerpt.buffer) - })) + })); }) .flatten() } @@ -5849,10 +5853,10 @@ impl MultiBufferSnapshot { let current_depth = indent_stack.len() as u32; // Avoid retrieving the language settings repeatedly for every buffer row. - if let Some((prev_buffer_id, _)) = &prev_settings - && prev_buffer_id != &buffer.remote_id() - { - prev_settings.take(); + if let Some((prev_buffer_id, _)) = &prev_settings { + if prev_buffer_id != &buffer.remote_id() { + prev_settings.take(); + } } let settings = &prev_settings .get_or_insert_with(|| { @@ -6181,10 +6185,10 @@ impl MultiBufferSnapshot { } else { let mut cursor = self.excerpt_ids.cursor::(&()); cursor.seek(&id, Bias::Left); - if let Some(entry) = cursor.item() - && entry.id == id - { - return &entry.locator; + if let Some(entry) = cursor.item() { + if entry.id == id { + return &entry.locator; + } } panic!("invalid excerpt id {id:?}") } @@ -6238,14 +6242,14 @@ impl MultiBufferSnapshot { pub fn range_for_excerpt(&self, excerpt_id: ExcerptId) -> Option> { let mut cursor = self .excerpts - .cursor::, ExcerptDimension>>(&()); + .cursor::<(Option<&Locator>, ExcerptDimension)>(&()); let locator = self.excerpt_locator_for_id(excerpt_id); if cursor.seek(&Some(locator), Bias::Left) { let start = cursor.start().1.clone(); let end = cursor.end().1; let mut diff_transforms = self .diff_transforms - .cursor::, OutputDimension>>(&()); + .cursor::<(ExcerptDimension, OutputDimension)>(&()); diff_transforms.seek(&start, Bias::Left); let overshoot = start.0 - diff_transforms.start().0.0; let start = diff_transforms.start().1.0 + overshoot; @@ -6261,10 +6265,10 @@ impl MultiBufferSnapshot { pub fn buffer_range_for_excerpt(&self, excerpt_id: ExcerptId) -> Option> { let mut cursor = self.excerpts.cursor::>(&()); let locator = self.excerpt_locator_for_id(excerpt_id); - if cursor.seek(&Some(locator), Bias::Left) - && let Some(excerpt) = cursor.item() - { - return Some(excerpt.range.context.clone()); + if cursor.seek(&Some(locator), Bias::Left) { + if let Some(excerpt) = cursor.item() { + return Some(excerpt.range.context.clone()); + } } None } @@ -6273,10 +6277,10 @@ impl MultiBufferSnapshot { let mut cursor = self.excerpts.cursor::>(&()); let locator = self.excerpt_locator_for_id(excerpt_id); cursor.seek(&Some(locator), Bias::Left); - if let Some(excerpt) = cursor.item() - && excerpt.id == excerpt_id - { - return Some(excerpt); + if let Some(excerpt) = cursor.item() { + if excerpt.id == excerpt_id { + return Some(excerpt); + } } None } @@ -6312,14 +6316,6 @@ impl MultiBufferSnapshot { }) } - pub fn buffer_id_for_anchor(&self, anchor: Anchor) -> Option { - if let Some(id) = anchor.buffer_id { - return Some(id); - } - let excerpt = self.excerpt_containing(anchor..anchor)?; - Some(excerpt.buffer_id()) - } - pub fn selections_in_range<'a>( &'a self, range: &'a Range, @@ -6415,7 +6411,7 @@ impl MultiBufferSnapshot { for (ix, entry) in excerpt_ids.iter().enumerate() { if ix == 0 { - if entry.id.cmp(&ExcerptId::min(), self).is_le() { + if entry.id.cmp(&ExcerptId::min(), &self).is_le() { panic!("invalid first excerpt id {:?}", entry.id); } } else if entry.id <= excerpt_ids[ix - 1].id { @@ -6443,12 +6439,13 @@ impl MultiBufferSnapshot { inserted_hunk_info: prev_inserted_hunk_info, .. }) = prev_transform - && *inserted_hunk_info == *prev_inserted_hunk_info { - panic!( - "multiple adjacent buffer content transforms with is_inserted_hunk = {inserted_hunk_info:?}. transforms: {:+?}", - self.diff_transforms.items(&()) - ); + if *inserted_hunk_info == *prev_inserted_hunk_info { + panic!( + "multiple adjacent buffer content transforms with is_inserted_hunk = {inserted_hunk_info:?}. transforms: {:+?}", + self.diff_transforms.items(&()) + ); + } } if summary.len == 0 && !self.is_empty() { panic!("empty buffer content transform"); @@ -6548,12 +6545,14 @@ where self.excerpts.next(); } else if let Some(DiffTransform::DeletedHunk { hunk_info, .. }) = self.diff_transforms.item() - && self + { + if self .excerpts .item() - .is_some_and(|excerpt| excerpt.id != hunk_info.excerpt_id) - { - self.excerpts.next(); + .map_or(false, |excerpt| excerpt.id != hunk_info.excerpt_id) + { + self.excerpts.next(); + } } } } @@ -6598,7 +6597,7 @@ where let prev_transform = self.diff_transforms.item(); self.diff_transforms.next(); - prev_transform.is_none_or(|next_transform| { + prev_transform.map_or(true, |next_transform| { matches!(next_transform, DiffTransform::BufferContent { .. }) }) } @@ -6613,12 +6612,12 @@ where } let next_transform = self.diff_transforms.next_item(); - next_transform.is_none_or(|next_transform| match next_transform { + next_transform.map_or(true, |next_transform| match next_transform { DiffTransform::BufferContent { .. } => true, DiffTransform::DeletedHunk { hunk_info, .. } => self .excerpts .item() - .is_some_and(|excerpt| excerpt.id != hunk_info.excerpt_id), + .map_or(false, |excerpt| excerpt.id != hunk_info.excerpt_id), }) } @@ -6642,7 +6641,7 @@ where hunk_info, .. } => { - let diff = self.diffs.get(buffer_id)?; + let diff = self.diffs.get(&buffer_id)?; let buffer = diff.base_text(); let mut rope_cursor = buffer.as_rope().cursor(0); let buffer_start = rope_cursor.summary::(base_text_byte_range.start); @@ -6651,7 +6650,7 @@ where buffer_end.add_assign(&buffer_range_len); let start = self.diff_transforms.start().output_dimension.0; let end = self.diff_transforms.end().output_dimension.0; - Some(MultiBufferRegion { + return Some(MultiBufferRegion { buffer, excerpt, has_trailing_newline: *has_trailing_newline, @@ -6661,7 +6660,7 @@ where )), buffer_range: buffer_start..buffer_end, range: start..end, - }) + }); } DiffTransform::BufferContent { inserted_hunk_info, .. @@ -6998,7 +6997,7 @@ impl Excerpt { } fn contains(&self, anchor: &Anchor) -> bool { - (anchor.buffer_id == None || anchor.buffer_id == Some(self.buffer_id)) + Some(self.buffer_id) == anchor.buffer_id && self .range .context @@ -7158,7 +7157,7 @@ impl ExcerptId { Self(usize::MAX) } - pub fn to_proto(self) -> u64 { + pub fn to_proto(&self) -> u64 { self.0 as _ } @@ -7499,59 +7498,61 @@ impl Iterator for MultiBufferRows<'_> { self.cursor.next(); if let Some(next_region) = self.cursor.region() { region = next_region; - } else if self.point == self.cursor.diff_transforms.end().output_dimension.0 { - let multibuffer_row = MultiBufferRow(self.point.row); - let last_excerpt = self - .cursor - .excerpts - .item() - .or(self.cursor.excerpts.prev_item())?; - let last_row = last_excerpt - .range - .context - .end - .to_point(&last_excerpt.buffer) - .row; - - let first_row = last_excerpt - .range - .context - .start - .to_point(&last_excerpt.buffer) - .row; - - let expand_info = if self.is_singleton { - None - } else { - let needs_expand_up = first_row == last_row - && last_row > 0 - && !region.diff_hunk_status.is_some_and(|d| d.is_deleted()); - let needs_expand_down = last_row < last_excerpt.buffer.max_point().row; - - if needs_expand_up && needs_expand_down { - Some(ExpandExcerptDirection::UpAndDown) - } else if needs_expand_up { - Some(ExpandExcerptDirection::Up) - } else if needs_expand_down { - Some(ExpandExcerptDirection::Down) - } else { - None - } - .map(|direction| ExpandInfo { - direction, - excerpt_id: last_excerpt.id, - }) - }; - self.point += Point::new(1, 0); - return Some(RowInfo { - buffer_id: Some(last_excerpt.buffer_id), - buffer_row: Some(last_row), - multibuffer_row: Some(multibuffer_row), - diff_status: None, - expand_info, - }); } else { - return None; + if self.point == self.cursor.diff_transforms.end().output_dimension.0 { + let multibuffer_row = MultiBufferRow(self.point.row); + let last_excerpt = self + .cursor + .excerpts + .item() + .or(self.cursor.excerpts.prev_item())?; + let last_row = last_excerpt + .range + .context + .end + .to_point(&last_excerpt.buffer) + .row; + + let first_row = last_excerpt + .range + .context + .start + .to_point(&last_excerpt.buffer) + .row; + + let expand_info = if self.is_singleton { + None + } else { + let needs_expand_up = first_row == last_row + && last_row > 0 + && !region.diff_hunk_status.is_some_and(|d| d.is_deleted()); + let needs_expand_down = last_row < last_excerpt.buffer.max_point().row; + + if needs_expand_up && needs_expand_down { + Some(ExpandExcerptDirection::UpAndDown) + } else if needs_expand_up { + Some(ExpandExcerptDirection::Up) + } else if needs_expand_down { + Some(ExpandExcerptDirection::Down) + } else { + None + } + .map(|direction| ExpandInfo { + direction, + excerpt_id: last_excerpt.id, + }) + }; + self.point += Point::new(1, 0); + return Some(RowInfo { + buffer_id: Some(last_excerpt.buffer_id), + buffer_row: Some(last_row), + multibuffer_row: Some(multibuffer_row), + diff_status: None, + expand_info, + }); + } else { + return None; + } }; } @@ -7759,7 +7760,7 @@ impl<'a> Iterator for MultiBufferChunks<'a> { } chunks } else { - let base_buffer = &self.diffs.get(buffer_id)?.base_text(); + let base_buffer = &self.diffs.get(&buffer_id)?.base_text(); base_buffer.chunks(base_text_start..base_text_end, self.language_aware) }; @@ -7847,11 +7848,10 @@ impl io::Read for ReversedMultiBufferBytes<'_> { if len > 0 { self.range.end -= len; self.chunk = &self.chunk[..self.chunk.len() - len]; - if !self.range.is_empty() - && self.chunk.is_empty() - && let Some(chunk) = self.chunks.next() - { - self.chunk = chunk.as_bytes(); + if !self.range.is_empty() && self.chunk.is_empty() { + if let Some(chunk) = self.chunks.next() { + self.chunk = chunk.as_bytes(); + } } } Ok(len) diff --git a/crates/multi_buffer/src/multi_buffer_tests.rs b/crates/multi_buffer/src/multi_buffer_tests.rs index 61b4b0520f..824efa559f 100644 --- a/crates/multi_buffer/src/multi_buffer_tests.rs +++ b/crates/multi_buffer/src/multi_buffer_tests.rs @@ -473,7 +473,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 +2250,11 @@ impl ReferenceMultibuffer { let base_buffer = diff.base_text(); let mut offset = buffer_range.start; - let hunks = diff + let mut hunks = diff .hunks_intersecting_range(excerpt.range.clone(), buffer, cx) .peekable(); - for hunk in hunks { + while let Some(hunk) = hunks.next() { // Ignore hunks that are outside the excerpt range. let mut hunk_range = hunk.buffer_range.to_offset(buffer); @@ -2265,14 +2265,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 +2449,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(); } @@ -2519,8 +2519,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() { @@ -2739,8 +2739,9 @@ 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) } @@ -3592,20 +3593,24 @@ 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 && *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], - ); + 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], + ); + } + } } } } diff --git a/crates/multi_buffer/src/position.rs b/crates/multi_buffer/src/position.rs index 8a3ce78d0d..0650875059 100644 --- a/crates/multi_buffer/src/position.rs +++ b/crates/multi_buffer/src/position.rs @@ -126,17 +126,17 @@ impl Default for TypedRow { impl PartialOrd for TypedOffset { fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) + Some(self.cmp(&other)) } } impl PartialOrd for TypedPoint { fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) + Some(self.cmp(&other)) } } impl PartialOrd for TypedRow { fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) + Some(self.cmp(&other)) } } diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index 9d41eb1562..08698a1d6c 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/crates/node_runtime/src/node_runtime.rs @@ -29,13 +29,6 @@ pub struct NodeBinaryOptions { pub use_paths: Option<(PathBuf, PathBuf)>, } -pub enum VersionStrategy<'a> { - /// Install if current version doesn't match pinned version - Pin(&'a str), - /// Install if current version is older than latest version - Latest(&'a str), -} - #[derive(Clone)] pub struct NodeRuntime(Arc>); @@ -76,8 +69,9 @@ impl NodeRuntime { let mut state = self.0.lock().await; let options = loop { - if let Some(options) = state.options.borrow().as_ref() { - break options.clone(); + match state.options.borrow().as_ref() { + Some(options) => break options.clone(), + None => {} } match state.options.changed().await { Ok(()) => {} @@ -196,7 +190,7 @@ impl NodeRuntime { state.instance = Some(instance.boxed_clone()); state.last_options = Some(options); - instance + return instance; } pub async fn binary_path(&self) -> Result { @@ -292,7 +286,7 @@ impl NodeRuntime { package_name: &str, local_executable_path: &Path, local_package_directory: &Path, - version_strategy: VersionStrategy<'_>, + latest_version: &str, ) -> bool { // In the case of the local system not having the package installed, // or in the instances where we fail to parse package.json data, @@ -313,21 +307,11 @@ impl NodeRuntime { let Some(installed_version) = Version::parse(&installed_version).log_err() else { return true; }; + let Some(latest_version) = Version::parse(latest_version).log_err() else { + return true; + }; - match version_strategy { - VersionStrategy::Pin(pinned_version) => { - let Some(pinned_version) = Version::parse(pinned_version).log_err() else { - return true; - }; - installed_version != pinned_version - } - VersionStrategy::Latest(latest_version) => { - let Some(latest_version) = Version::parse(latest_version).log_err() else { - return true; - }; - installed_version < latest_version - } - } + installed_version < latest_version } } diff --git a/crates/notifications/src/notification_store.rs b/crates/notifications/src/notification_store.rs index af2601bd18..0329a53cc7 100644 --- a/crates/notifications/src/notification_store.rs +++ b/crates/notifications/src/notification_store.rs @@ -6,7 +6,7 @@ use db::smol::stream::StreamExt; use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Global, Task}; use rpc::{Notification, TypedEnvelope, proto}; use std::{ops::Range, sync::Arc}; -use sum_tree::{Bias, Dimensions, SumTree}; +use sum_tree::{Bias, SumTree}; use time::OffsetDateTime; use util::ResultExt; @@ -138,10 +138,10 @@ impl NotificationStore { pub fn notification_for_id(&self, id: u64) -> Option<&NotificationEntry> { let mut cursor = self.notifications.cursor::(&()); cursor.seek(&NotificationId(id), Bias::Left); - if let Some(item) = cursor.item() - && item.id == id - { - return Some(item); + if let Some(item) = cursor.item() { + if item.id == id { + return Some(item); + } } None } @@ -229,24 +229,25 @@ impl NotificationStore { mut cx: AsyncApp, ) -> Result<()> { this.update(&mut cx, |this, cx| { - if let Some(notification) = envelope.payload.notification - && let Some(rpc::Notification::ChannelMessageMention { message_id, .. }) = + if let Some(notification) = envelope.payload.notification { + if let Some(rpc::Notification::ChannelMessageMention { message_id, .. }) = Notification::from_proto(¬ification) - { - let fetch_message_task = this.channel_store.update(cx, |this, cx| { - this.fetch_channel_messages(vec![message_id], cx) - }); + { + let fetch_message_task = this.channel_store.update(cx, |this, cx| { + this.fetch_channel_messages(vec![message_id], cx) + }); - cx.spawn(async move |this, cx| { - let messages = fetch_message_task.await?; - this.update(cx, move |this, cx| { - for message in messages { - this.channel_messages.insert(message_id, message); - } - cx.notify(); + cx.spawn(async move |this, cx| { + let messages = fetch_message_task.await?; + this.update(cx, move |this, cx| { + for message in messages { + this.channel_messages.insert(message_id, message); + } + cx.notify(); + }) }) - }) - .detach_and_log_err(cx) + .detach_and_log_err(cx) + } } Ok(()) })? @@ -359,9 +360,7 @@ impl NotificationStore { is_new: bool, cx: &mut Context, ) { - let mut cursor = self - .notifications - .cursor::>(&()); + let mut cursor = self.notifications.cursor::<(NotificationId, Count)>(&()); let mut new_notifications = SumTree::default(); let mut old_range = 0..0; @@ -389,12 +388,12 @@ impl NotificationStore { }); } } - } else if let Some(new_notification) = &new_notification - && is_new - { - cx.emit(NotificationEvent::NewNotification { - entry: new_notification.clone(), - }); + } else if let Some(new_notification) = &new_notification { + if is_new { + cx.emit(NotificationEvent::NewNotification { + entry: new_notification.clone(), + }); + } } if let Some(notification) = new_notification { diff --git a/crates/notifications/src/status_toast.rs b/crates/notifications/src/status_toast.rs index 7affa93f5a..ffd87e0b8b 100644 --- a/crates/notifications/src/status_toast.rs +++ b/crates/notifications/src/status_toast.rs @@ -205,7 +205,7 @@ impl Component for StatusToast { let pr_example = StatusToast::new("`zed/new-notification-system` created!", cx, |this, _cx| { - this.icon(ToastIcon::new(IconName::GitBranchAlt).color(Color::Muted)) + this.icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted)) .action("Open Pull Request", |_, cx| { cx.open_url("https://github.com/") }) diff --git a/crates/ollama/src/ollama.rs b/crates/ollama/src/ollama.rs index 64cd1cc0cb..62c32b4161 100644 --- a/crates/ollama/src/ollama.rs +++ b/crates/ollama/src/ollama.rs @@ -58,7 +58,7 @@ fn get_max_tokens(name: &str) -> u64 { "magistral" => 40000, "llama3.1" | "llama3.2" | "llama3.3" | "phi3" | "phi3.5" | "phi4" | "command-r" | "qwen3" | "gemma3" | "deepseek-coder-v2" | "deepseek-v3" | "deepseek-r1" | "yi-coder" - | "devstral" | "gpt-oss" => 128000, + | "devstral" => 128000, _ => DEFAULT_TOKENS, } .clamp(1, MAXIMUM_TOKENS) diff --git a/crates/onboarding/Cargo.toml b/crates/onboarding/Cargo.toml index 4157be3172..7727597e94 100644 --- a/crates/onboarding/Cargo.toml +++ b/crates/onboarding/Cargo.toml @@ -15,28 +15,21 @@ path = "src/onboarding.rs" default = [] [dependencies] -ai_onboarding.workspace = true anyhow.workspace = true client.workspace = true +command_palette_hooks.workspace = true component.workspace = true -db.workspace = true documented.workspace = true +db.workspace = true editor.workspace = true +feature_flags.workspace = true fs.workspace = true -fuzzy.workspace = true -git.workspace = true gpui.workspace = true -itertools.workspace = true language.workspace = true -language_model.workspace = true -menu.workspace = true -notifications.workspace = true -picker.workspace = true project.workspace = true schemars.workspace = true serde.workspace = true settings.workspace = true -telemetry.workspace = true theme.workspace = true ui.workspace = true util.workspace = true diff --git a/crates/onboarding/src/ai_setup_page.rs b/crates/onboarding/src/ai_setup_page.rs deleted file mode 100644 index 54c49bc72a..0000000000 --- a/crates/onboarding/src/ai_setup_page.rs +++ /dev/null @@ -1,431 +0,0 @@ -use std::sync::Arc; - -use ai_onboarding::AiUpsellCard; -use client::{Client, UserStore, zed_urls}; -use fs::Fs; -use gpui::{ - Action, AnyView, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity, - Window, prelude::*, -}; -use itertools; -use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry}; -use project::DisableAiSettings; -use settings::{Settings, update_settings_file}; -use ui::{ - Badge, ButtonLike, Divider, KeyBinding, Modal, ModalFooter, ModalHeader, Section, SwitchField, - ToggleState, prelude::*, tooltip_container, -}; -use util::ResultExt; -use workspace::{ModalView, Workspace}; -use zed_actions::agent::OpenSettings; - -const FEATURED_PROVIDERS: [&str; 4] = ["anthropic", "google", "openai", "ollama"]; - -fn render_llm_provider_section( - tab_index: &mut isize, - workspace: WeakEntity, - disabled: bool, - window: &mut Window, - cx: &mut App, -) -> impl IntoElement { - v_flex() - .gap_4() - .child( - v_flex() - .child(Label::new("Or use other LLM providers").size(LabelSize::Large)) - .child( - Label::new("Bring your API keys to use the available providers with Zed's UI for free.") - .color(Color::Muted), - ), - ) - .child(render_llm_provider_card(tab_index, workspace, disabled, window, cx)) -} - -fn render_privacy_card(tab_index: &mut isize, disabled: bool, cx: &mut App) -> impl IntoElement { - let (title, description) = if disabled { - ( - "AI is disabled across Zed", - "Re-enable it any time in Settings.", - ) - } else { - ( - "Privacy is the default for Zed", - "Any use or storage of your data is with your explicit, single-use, opt-in consent.", - ) - }; - - v_flex() - .relative() - .pt_2() - .pb_2p5() - .pl_3() - .pr_2() - .border_1() - .border_dashed() - .border_color(cx.theme().colors().border.opacity(0.5)) - .bg(cx.theme().colors().surface_background.opacity(0.3)) - .rounded_lg() - .overflow_hidden() - .child( - h_flex() - .gap_2() - .justify_between() - .child(Label::new(title)) - .child( - h_flex() - .gap_1() - .child( - Badge::new("Privacy") - .icon(IconName::ShieldCheck) - .tooltip(move |_, cx| cx.new(|_| AiPrivacyTooltip::new()).into()), - ) - .child( - Button::new("learn_more", "Learn More") - .style(ButtonStyle::Outlined) - .label_size(LabelSize::Small) - .icon(IconName::ArrowUpRight) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .on_click(|_, _, cx| { - cx.open_url(&zed_urls::ai_privacy_and_security(cx)) - }) - .tab_index({ - *tab_index += 1; - *tab_index - 1 - }), - ), - ), - ) - .child( - Label::new(description) - .size(LabelSize::Small) - .color(Color::Muted), - ) -} - -fn render_llm_provider_card( - tab_index: &mut isize, - workspace: WeakEntity, - disabled: bool, - _: &mut Window, - cx: &mut App, -) -> impl IntoElement { - let registry = LanguageModelRegistry::read_global(cx); - - v_flex() - .border_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().surface_background.opacity(0.5)) - .rounded_lg() - .overflow_hidden() - .children(itertools::intersperse_with( - FEATURED_PROVIDERS - .into_iter() - .flat_map(|provider_name| { - registry.provider(&LanguageModelProviderId::new(provider_name)) - }) - .enumerate() - .map(|(index, provider)| { - let group_name = SharedString::new(format!("onboarding-hover-group-{}", index)); - let is_authenticated = provider.is_authenticated(cx); - - ButtonLike::new(("onboarding-ai-setup-buttons", index)) - .size(ButtonSize::Large) - .tab_index({ - *tab_index += 1; - *tab_index - 1 - }) - .child( - h_flex() - .group(&group_name) - .px_0p5() - .w_full() - .gap_2() - .justify_between() - .child( - h_flex() - .gap_1() - .child( - Icon::new(provider.icon()) - .color(Color::Muted) - .size(IconSize::XSmall), - ) - .child(Label::new(provider.name().0)), - ) - .child( - h_flex() - .gap_1() - .when(!is_authenticated, |el| { - el.visible_on_hover(group_name.clone()) - .child( - Icon::new(IconName::Settings) - .color(Color::Muted) - .size(IconSize::XSmall), - ) - .child( - Label::new("Configure") - .color(Color::Muted) - .size(LabelSize::Small), - ) - }) - .when(is_authenticated && !disabled, |el| { - el.child( - Icon::new(IconName::Check) - .color(Color::Success) - .size(IconSize::XSmall), - ) - .child( - Label::new("Configured") - .color(Color::Muted) - .size(LabelSize::Small), - ) - }), - ), - ) - .on_click({ - let workspace = workspace.clone(); - move |_, window, cx| { - workspace - .update(cx, |workspace, cx| { - workspace.toggle_modal(window, cx, |window, cx| { - telemetry::event!( - "Welcome AI Modal Opened", - provider = provider.name().0, - ); - - let modal = AiConfigurationModal::new( - provider.clone(), - window, - cx, - ); - window.focus(&modal.focus_handle(cx)); - modal - }); - }) - .log_err(); - } - }) - .into_any_element() - }), - || Divider::horizontal().into_any_element(), - )) - .child(Divider::horizontal()) - .child( - Button::new("agent_settings", "Add Many Others") - .size(ButtonSize::Large) - .icon(IconName::Plus) - .icon_position(IconPosition::Start) - .icon_color(Color::Muted) - .icon_size(IconSize::XSmall) - .on_click(|_event, window, cx| { - window.dispatch_action(OpenSettings.boxed_clone(), cx) - }) - .tab_index({ - *tab_index += 1; - *tab_index - 1 - }), - ) -} - -pub(crate) fn render_ai_setup_page( - workspace: WeakEntity, - user_store: Entity, - client: Arc, - window: &mut Window, - cx: &mut App, -) -> impl IntoElement { - let mut tab_index = 0; - let is_ai_disabled = DisableAiSettings::get_global(cx).disable_ai; - - v_flex() - .gap_2() - .child( - SwitchField::new( - "enable_ai", - "Enable AI features", - None, - if is_ai_disabled { - ToggleState::Unselected - } else { - ToggleState::Selected - }, - |&toggle_state, _, cx| { - let enabled = match toggle_state { - ToggleState::Indeterminate => { - return; - } - ToggleState::Unselected => true, - ToggleState::Selected => false, - }; - - telemetry::event!( - "Welcome AI Enabled", - toggle = if enabled { "on" } else { "off" }, - ); - - let fs = ::global(cx); - update_settings_file::( - fs, - cx, - move |ai_settings: &mut Option, _| { - *ai_settings = Some(enabled); - }, - ); - }, - ) - .tab_index({ - tab_index += 1; - tab_index - 1 - }), - ) - .child(render_privacy_card(&mut tab_index, is_ai_disabled, cx)) - .child( - v_flex() - .mt_2() - .gap_6() - .child( - AiUpsellCard::new(client, &user_store, user_store.read(cx).plan(), cx) - .tab_index(Some({ - tab_index += 1; - tab_index - 1 - })), - ) - .child(render_llm_provider_section( - &mut tab_index, - workspace, - is_ai_disabled, - window, - cx, - )) - .when(is_ai_disabled, |this| { - this.child( - div() - .id("backdrop") - .size_full() - .absolute() - .inset_0() - .bg(cx.theme().colors().editor_background) - .opacity(0.8) - .block_mouse_except_scroll(), - ) - }), - ) -} - -struct AiConfigurationModal { - focus_handle: FocusHandle, - selected_provider: Arc, - configuration_view: AnyView, -} - -impl AiConfigurationModal { - fn new( - selected_provider: Arc, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let focus_handle = cx.focus_handle(); - let configuration_view = selected_provider.configuration_view( - language_model::ConfigurationViewTargetAgent::ZedAgent, - window, - cx, - ); - - Self { - focus_handle, - configuration_view, - selected_provider, - } - } - - fn cancel(&mut self, _: &menu::Cancel, cx: &mut Context) { - cx.emit(DismissEvent); - } -} - -impl ModalView for AiConfigurationModal {} - -impl EventEmitter for AiConfigurationModal {} - -impl Focusable for AiConfigurationModal { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl Render for AiConfigurationModal { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - v_flex() - .key_context("OnboardingAiConfigurationModal") - .w(rems(34.)) - .elevation_3(cx) - .track_focus(&self.focus_handle) - .on_action( - cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(&menu::Cancel, cx)), - ) - .child( - Modal::new("onboarding-ai-setup-modal", None) - .header( - ModalHeader::new() - .icon( - Icon::new(self.selected_provider.icon()) - .color(Color::Muted) - .size(IconSize::Small), - ) - .headline(self.selected_provider.name().0), - ) - .section(Section::new().child(self.configuration_view.clone())) - .footer( - ModalFooter::new().end_slot( - Button::new("ai-onb-modal-Done", "Done") - .key_binding( - KeyBinding::for_action_in( - &menu::Cancel, - &self.focus_handle.clone(), - window, - cx, - ) - .map(|kb| kb.size(rems_from_px(12.))), - ) - .on_click(cx.listener(|this, _event, _window, cx| { - this.cancel(&menu::Cancel, cx) - })), - ), - ), - ) - } -} - -pub struct AiPrivacyTooltip {} - -impl AiPrivacyTooltip { - pub fn new() -> Self { - Self {} - } -} - -impl Render for AiPrivacyTooltip { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - const DESCRIPTION: &str = "We believe in opt-in data sharing as the default for building AI products, rather than opt-out. We'll only use or store your data if you affirmatively send it to us. "; - - tooltip_container(window, cx, move |this, _, _| { - this.child( - h_flex() - .gap_1() - .child( - Icon::new(IconName::ShieldCheck) - .size(IconSize::Small) - .color(Color::Muted), - ) - .child(Label::new("Privacy First")), - ) - .child( - div().max_w_64().child( - Label::new(DESCRIPTION) - .size(LabelSize::Small) - .color(Color::Muted), - ), - ) - }) - } -} diff --git a/crates/onboarding/src/basics_page.rs b/crates/onboarding/src/basics_page.rs index 441d2ca4b7..bfbe0374d3 100644 --- a/crates/onboarding/src/basics_page.rs +++ b/crates/onboarding/src/basics_page.rs @@ -1,41 +1,42 @@ -use std::sync::Arc; - use client::TelemetrySettings; use fs::Fs; -use gpui::{App, IntoElement}; +use gpui::{App, Entity, IntoElement, Window}; use settings::{BaseKeymap, Settings, update_settings_file}; -use theme::{ - Appearance, SystemAppearance, ThemeMode, ThemeName, ThemeRegistry, ThemeSelection, - ThemeSettings, -}; +use theme::{Appearance, ThemeMode, ThemeName, ThemeRegistry, ThemeSelection, ThemeSettings}; use ui::{ ParentElement as _, StatefulInteractiveElement, SwitchField, ToggleButtonGroup, ToggleButtonSimple, ToggleButtonWithIcon, prelude::*, rems_from_px, }; use vim_mode_setting::VimModeSetting; -use crate::theme_preview::{ThemePreviewStyle, ThemePreviewTile}; +use crate::theme_preview::ThemePreviewTile; -const LIGHT_THEMES: [&str; 3] = ["One Light", "Ayu Light", "Gruvbox Light"]; -const DARK_THEMES: [&str; 3] = ["One Dark", "Ayu Dark", "Gruvbox Dark"]; -const FAMILY_NAMES: [SharedString; 3] = [ - SharedString::new_static("One"), - SharedString::new_static("Ayu"), - SharedString::new_static("Gruvbox"), -]; - -fn get_theme_family_themes(theme_name: &str) -> Option<(&'static str, &'static str)> { - for i in 0..LIGHT_THEMES.len() { - if LIGHT_THEMES[i] == theme_name || DARK_THEMES[i] == theme_name { - return Some((LIGHT_THEMES[i], DARK_THEMES[i])); - } - } - None -} - -fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement { +/// separates theme "mode" ("dark" | "light" | "system") into two separate states +/// - appearance = "dark" | "light" +/// - "system" true/false +/// when system selected: +/// - toggling between light and dark does not change theme.mode, just which variant will be changed +/// when system not selected: +/// - toggling between light and dark does change theme.mode +/// selecting a theme preview will always change theme.["light" | "dark"] to the selected theme, +/// +/// this allows for selecting a dark and light theme option regardless of whether the mode is set to system or not +/// it does not support setting theme to a static value +fn render_theme_section(window: &mut Window, cx: &mut App) -> impl IntoElement { let theme_selection = ThemeSettings::get_global(cx).theme_selection.clone(); let system_appearance = theme::SystemAppearance::global(cx); + let appearance_state = window.use_state(cx, |_, _cx| { + theme_selection + .as_ref() + .and_then(|selection| selection.mode()) + .and_then(|mode| match mode { + ThemeMode::System => None, + ThemeMode::Light => Some(Appearance::Light), + ThemeMode::Dark => Some(Appearance::Dark), + }) + .unwrap_or(*system_appearance) + }); + let appearance = *appearance_state.read(cx); let theme_selection = theme_selection.unwrap_or_else(|| ThemeSelection::Dynamic { mode: match *system_appearance { Appearance::Light => ThemeMode::Light, @@ -44,182 +45,204 @@ fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement light: ThemeName("One Light".into()), dark: ThemeName("One Dark".into()), }); + let theme_registry = ThemeRegistry::global(cx); - let theme_mode = theme_selection - .mode() - .unwrap_or_else(|| match *system_appearance { - Appearance::Light => ThemeMode::Light, - Appearance::Dark => ThemeMode::Dark, - }); + let current_theme_name = theme_selection.theme(appearance); + let theme_mode = theme_selection.mode(); + + let selected_index = match appearance { + Appearance::Light => 0, + Appearance::Dark => 1, + }; + + let theme_seed = 0xBEEF as f32; + + const LIGHT_THEMES: [&'static str; 3] = ["One Light", "Ayu Light", "Gruvbox Light"]; + const DARK_THEMES: [&'static str; 3] = ["One Dark", "Ayu Dark", "Gruvbox Dark"]; + + let theme_names = match appearance { + Appearance::Light => LIGHT_THEMES, + Appearance::Dark => DARK_THEMES, + }; + let themes = theme_names + .map(|theme_name| theme_registry.get(theme_name)) + .map(Result::unwrap); + + let theme_previews = themes.map(|theme| { + let is_selected = theme.name == current_theme_name; + let name = theme.name.clone(); + let colors = cx.theme().colors(); + v_flex() + .id(name.clone()) + .on_click({ + let theme_name = theme.name.clone(); + move |_, _, cx| { + let fs = ::global(cx); + let theme_name = theme_name.clone(); + update_settings_file::(fs, cx, move |settings, _| { + settings.set_theme(theme_name, appearance); + }); + } + }) + .flex_1() + .child( + div() + .border_2() + .border_color(colors.border_transparent) + .rounded(ThemePreviewTile::CORNER_RADIUS) + .hover(|mut style| { + if !is_selected { + style.border_color = Some(colors.element_hover); + } + style + }) + .when(is_selected, |this| { + this.border_color(colors.border_selected) + }) + .cursor_pointer() + .child(ThemePreviewTile::new(theme, theme_seed)), + ) + .child( + h_flex() + .justify_center() + .items_baseline() + .child(Label::new(name).color(Color::Muted)), + ) + }); return v_flex() - .gap_2() .child( h_flex().justify_between().child(Label::new("Theme")).child( - ToggleButtonGroup::single_row( - "theme-selector-onboarding-dark-light", - [ThemeMode::Light, ThemeMode::Dark, ThemeMode::System].map(|mode| { - const MODE_NAMES: [SharedString; 3] = [ - SharedString::new_static("Light"), - SharedString::new_static("Dark"), - SharedString::new_static("System"), - ]; - ToggleButtonSimple::new( - MODE_NAMES[mode as usize].clone(), - move |_, _, cx| { - write_mode_change(mode, cx); - }, + h_flex() + .gap_2() + .child( + ToggleButtonGroup::single_row( + "theme-selector-onboarding-dark-light", + [ + ToggleButtonSimple::new("Light", { + let appearance_state = appearance_state.clone(); + move |_, _, cx| { + write_appearance_change( + &appearance_state, + Appearance::Light, + cx, + ); + } + }), + ToggleButtonSimple::new("Dark", { + let appearance_state = appearance_state.clone(); + move |_, _, cx| { + write_appearance_change( + &appearance_state, + Appearance::Dark, + cx, + ); + } + }), + ], ) - }), - ) - .tab_index(tab_index) - .selected_index(theme_mode as usize) - .style(ui::ToggleButtonGroupStyle::Outlined) - .width(rems_from_px(3. * 64.)), + .selected_index(selected_index) + .style(ui::ToggleButtonGroupStyle::Outlined) + .button_width(rems_from_px(64.)), + ) + .child( + ToggleButtonGroup::single_row( + "theme-selector-onboarding-system", + [ToggleButtonSimple::new("System", { + let theme = theme_selection.clone(); + move |_, _, cx| { + toggle_system_theme_mode(theme.clone(), appearance, cx); + } + })], + ) + .selected_index((theme_mode != Some(ThemeMode::System)) as usize) + .style(ui::ToggleButtonGroupStyle::Outlined) + .button_width(rems_from_px(64.)), + ), ), ) - .child( - h_flex() - .gap_4() - .justify_between() - .children(render_theme_previews(tab_index, &theme_selection, cx)), - ); + .child(h_flex().justify_between().children(theme_previews)); - fn render_theme_previews( - tab_index: &mut isize, - theme_selection: &ThemeSelection, + fn write_appearance_change( + appearance_state: &Entity, + new_appearance: Appearance, cx: &mut App, - ) -> [impl IntoElement; 3] { - let system_appearance = SystemAppearance::global(cx); - let theme_registry = ThemeRegistry::global(cx); + ) { + appearance_state.update(cx, |appearance, _| { + *appearance = new_appearance; + }); + let fs = ::global(cx); - let theme_seed = 0xBEEF as f32; - let theme_mode = theme_selection - .mode() - .unwrap_or_else(|| match *system_appearance { + update_settings_file::(fs, cx, move |settings, _| { + if settings.theme.as_ref().and_then(ThemeSelection::mode) == Some(ThemeMode::System) { + return; + } + let new_mode = match new_appearance { Appearance::Light => ThemeMode::Light, Appearance::Dark => ThemeMode::Dark, - }); - let appearance = match theme_mode { - ThemeMode::Light => Appearance::Light, - ThemeMode::Dark => Appearance::Dark, - ThemeMode::System => *system_appearance, - }; - let current_theme_name = theme_selection.theme(appearance); - - let theme_names = match appearance { - Appearance::Light => LIGHT_THEMES, - Appearance::Dark => DARK_THEMES, - }; - - let themes = theme_names.map(|theme| theme_registry.get(theme).unwrap()); - - [0, 1, 2].map(|index| { - let theme = &themes[index]; - let is_selected = theme.name == current_theme_name; - let name = theme.name.clone(); - let colors = cx.theme().colors(); - - v_flex() - .w_full() - .items_center() - .gap_1() - .child( - h_flex() - .id(name) - .relative() - .w_full() - .border_2() - .border_color(colors.border_transparent) - .rounded(ThemePreviewTile::ROOT_RADIUS) - .map(|this| { - if is_selected { - this.border_color(colors.border_selected) - } else { - this.opacity(0.8).hover(|s| s.border_color(colors.border)) - } - }) - .tab_index({ - *tab_index += 1; - *tab_index - 1 - }) - .focus(|mut style| { - style.border_color = Some(colors.border_focused); - style - }) - .on_click({ - let theme_name = theme.name.clone(); - move |_, _, cx| { - write_theme_change(theme_name.clone(), theme_mode, cx); - } - }) - .map(|this| { - if theme_mode == ThemeMode::System { - let (light, dark) = ( - theme_registry.get(LIGHT_THEMES[index]).unwrap(), - theme_registry.get(DARK_THEMES[index]).unwrap(), - ); - this.child( - ThemePreviewTile::new(light, theme_seed) - .style(ThemePreviewStyle::SideBySide(dark)), - ) - } else { - this.child( - ThemePreviewTile::new(theme.clone(), theme_seed) - .style(ThemePreviewStyle::Bordered), - ) - } - }), - ) - .child( - Label::new(FAMILY_NAMES[index].clone()) - .color(Color::Muted) - .size(LabelSize::Small), - ) - }) - } - - fn write_mode_change(mode: ThemeMode, cx: &mut App) { - let fs = ::global(cx); - update_settings_file::(fs, cx, move |settings, _cx| { - settings.set_mode(mode); + }; + settings.set_mode(new_mode); }); } - fn write_theme_change(theme: impl Into>, theme_mode: ThemeMode, cx: &mut App) { + fn toggle_system_theme_mode( + theme_selection: ThemeSelection, + appearance: Appearance, + cx: &mut App, + ) { let fs = ::global(cx); - let theme = theme.into(); - update_settings_file::(fs, cx, move |settings, cx| { - if theme_mode == ThemeMode::System { - let (light_theme, dark_theme) = - get_theme_family_themes(&theme).unwrap_or((theme.as_ref(), theme.as_ref())); - settings.theme = Some(ThemeSelection::Dynamic { + update_settings_file::(fs, cx, move |settings, _| { + settings.theme = Some(match theme_selection { + ThemeSelection::Static(theme_name) => ThemeSelection::Dynamic { mode: ThemeMode::System, - light: ThemeName(light_theme.into()), - dark: ThemeName(dark_theme.into()), - }); - } else { - let appearance = *SystemAppearance::global(cx); - settings.set_theme(theme, appearance); - } + light: theme_name.clone(), + dark: theme_name.clone(), + }, + ThemeSelection::Dynamic { + mode: ThemeMode::System, + light, + dark, + } => { + let mode = match appearance { + Appearance::Light => ThemeMode::Light, + Appearance::Dark => ThemeMode::Dark, + }; + ThemeSelection::Dynamic { mode, light, dark } + } + + ThemeSelection::Dynamic { + mode: _, + light, + dark, + } => ThemeSelection::Dynamic { + mode: ThemeMode::System, + light, + dark, + }, + }); }); } } -fn render_telemetry_section(tab_index: &mut isize, cx: &App) -> impl IntoElement { +fn write_keymap_base(keymap_base: BaseKeymap, cx: &App) { + let fs = ::global(cx); + + update_settings_file::(fs, cx, move |setting, _| { + *setting = Some(keymap_base); + }); +} + +fn render_telemetry_section(cx: &App) -> impl IntoElement { let fs = ::global(cx); v_flex() - .pt_6() .gap_4() - .border_t_1() - .border_color(cx.theme().colors().border_variant.opacity(0.5)) .child(Label::new("Telemetry").size(LabelSize::Large)) .child(SwitchField::new( "onboarding-telemetry-metrics", "Help Improve Zed", - Some("Anonymous usage data helps us build the right features and improve your experience.".into()), + "Sending anonymous usage data helps us build the right features and create the best experience.", if TelemetrySettings::get_global(cx).metrics { ui::ToggleState::Selected } else { @@ -240,14 +263,11 @@ fn render_telemetry_section(tab_index: &mut isize, cx: &App) -> impl IntoElement move |setting, _| setting.metrics = Some(enabled), ); }}, - ).tab_index({ - *tab_index += 1; - *tab_index - })) + )) .child(SwitchField::new( "onboarding-telemetry-crash-reports", "Help Fix Zed", - Some("Send crash reports so we can fix critical issues fast.".into()), + "Send crash reports so we can fix critical issues fast.", if TelemetrySettings::get_global(cx).diagnostics { ui::ToggleState::Selected } else { @@ -269,13 +289,10 @@ fn render_telemetry_section(tab_index: &mut isize, cx: &App) -> impl IntoElement ); } } - ).tab_index({ - *tab_index += 1; - *tab_index - })) + )) } -fn render_base_keymap_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement { +pub(crate) fn render_basics_page(window: &mut Window, cx: &mut App) -> impl IntoElement { let base_keymap = match BaseKeymap::get_global(cx) { BaseKeymap::VSCode => Some(0), BaseKeymap::JetBrains => Some(1), @@ -286,86 +303,66 @@ fn render_base_keymap_section(tab_index: &mut isize, cx: &mut App) -> impl IntoE BaseKeymap::TextMate | BaseKeymap::None => None, }; - return v_flex().gap_2().child(Label::new("Base Keymap")).child( - ToggleButtonGroup::two_rows( - "base_keymap_selection", - [ - ToggleButtonWithIcon::new("VS Code", IconName::EditorVsCode, |_, _, cx| { - write_keymap_base(BaseKeymap::VSCode, cx); - }), - ToggleButtonWithIcon::new("Jetbrains", IconName::EditorJetBrains, |_, _, cx| { - write_keymap_base(BaseKeymap::JetBrains, cx); - }), - ToggleButtonWithIcon::new("Sublime Text", IconName::EditorSublime, |_, _, cx| { - write_keymap_base(BaseKeymap::SublimeText, cx); - }), - ], - [ - ToggleButtonWithIcon::new("Atom", IconName::EditorAtom, |_, _, cx| { - write_keymap_base(BaseKeymap::Atom, cx); - }), - ToggleButtonWithIcon::new("Emacs", IconName::EditorEmacs, |_, _, cx| { - write_keymap_base(BaseKeymap::Emacs, cx); - }), - ToggleButtonWithIcon::new("Cursor", IconName::EditorCursor, |_, _, cx| { - write_keymap_base(BaseKeymap::Cursor, cx); - }), - ], - ) - .when_some(base_keymap, |this, base_keymap| { - this.selected_index(base_keymap) - }) - .full_width() - .tab_index(tab_index) - .size(ui::ToggleButtonGroupSize::Medium) - .style(ui::ToggleButtonGroupStyle::Outlined), - ); - - fn write_keymap_base(keymap_base: BaseKeymap, cx: &App) { - let fs = ::global(cx); - - update_settings_file::(fs, cx, move |setting, _| { - *setting = Some(keymap_base); - }); - } -} - -fn render_vim_mode_switch(tab_index: &mut isize, cx: &mut App) -> impl IntoElement { - let toggle_state = if VimModeSetting::get_global(cx).0 { - ui::ToggleState::Selected - } else { - ui::ToggleState::Unselected - }; - SwitchField::new( - "onboarding-vim-mode", - "Vim Mode", - Some("Coming from Neovim? Use our first-class implementation of Vim Mode.".into()), - toggle_state, - { - let fs = ::global(cx); - move |&selection, _, cx| { - update_settings_file::(fs.clone(), cx, move |setting, _| { - *setting = match selection { - ToggleState::Selected => Some(true), - ToggleState::Unselected => Some(false), - ToggleState::Indeterminate => None, - } - }); - } - }, - ) - .tab_index({ - *tab_index += 1; - *tab_index - 1 - }) -} - -pub(crate) fn render_basics_page(cx: &mut App) -> impl IntoElement { - let mut tab_index = 0; v_flex() .gap_6() - .child(render_theme_section(&mut tab_index, cx)) - .child(render_base_keymap_section(&mut tab_index, cx)) - .child(render_vim_mode_switch(&mut tab_index, cx)) - .child(render_telemetry_section(&mut tab_index, cx)) + .child(render_theme_section(window, cx)) + .child( + v_flex().gap_2().child(Label::new("Base Keymap")).child( + ToggleButtonGroup::two_rows( + "multiple_row_test", + [ + ToggleButtonWithIcon::new("VS Code", IconName::AiZed, |_, _, cx| { + write_keymap_base(BaseKeymap::VSCode, cx); + }), + ToggleButtonWithIcon::new("Jetbrains", IconName::AiZed, |_, _, cx| { + write_keymap_base(BaseKeymap::JetBrains, cx); + }), + ToggleButtonWithIcon::new("Sublime Text", IconName::AiZed, |_, _, cx| { + write_keymap_base(BaseKeymap::SublimeText, cx); + }), + ], + [ + ToggleButtonWithIcon::new("Atom", IconName::AiZed, |_, _, cx| { + write_keymap_base(BaseKeymap::Atom, cx); + }), + ToggleButtonWithIcon::new("Emacs", IconName::AiZed, |_, _, cx| { + write_keymap_base(BaseKeymap::Emacs, cx); + }), + ToggleButtonWithIcon::new("Cursor (Beta)", IconName::AiZed, |_, _, cx| { + write_keymap_base(BaseKeymap::Cursor, cx); + }), + ], + ) + .when_some(base_keymap, |this, base_keymap| this.selected_index(base_keymap)) + .button_width(rems_from_px(230.)) + .style(ui::ToggleButtonGroupStyle::Outlined) + ), + ) + .child(v_flex().justify_center().child(div().h_0().child("hack").invisible()).child(SwitchField::new( + "onboarding-vim-mode", + "Vim Mode", + "Coming from Neovim? Zed's first-class implementation of Vim Mode has got your back.", + if VimModeSetting::get_global(cx).0 { + ui::ToggleState::Selected + } else { + ui::ToggleState::Unselected + }, + { + let fs = ::global(cx); + move |selection, _, cx| { + let enabled = match selection { + ToggleState::Selected => true, + ToggleState::Unselected => false, + ToggleState::Indeterminate => { return; }, + }; + + update_settings_file::( + fs.clone(), + cx, + move |setting, _| *setting = Some(enabled), + ); + } + }, + ))) + .child(render_telemetry_section(cx)) } diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index 47dfd84894..3fb9aaf0cc 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -1,23 +1,16 @@ -use std::sync::Arc; - use editor::{EditorSettings, ShowMinimap}; use fs::Fs; -use fuzzy::{StringMatch, StringMatchCandidate}; -use gpui::{ - Action, AnyElement, App, Context, FontFeatures, IntoElement, Pixels, SharedString, Task, Window, -}; -use language::language_settings::{AllLanguageSettings, FormatOnSave}; -use picker::{Picker, PickerDelegate}; +use gpui::{Action, App, IntoElement, Pixels, Window}; +use language::language_settings::AllLanguageSettings; use project::project_settings::ProjectSettings; use settings::{Settings as _, update_settings_file}; use theme::{FontFamilyCache, FontFamilyName, ThemeSettings}; use ui::{ - ButtonLike, ListItem, ListItemSpacing, NumericStepper, PopoverMenu, SwitchField, - ToggleButtonGroup, ToggleButtonGroupStyle, ToggleButtonSimple, ToggleState, Tooltip, - prelude::*, + ButtonLike, ContextMenu, DropdownMenu, NumericStepper, SwitchField, ToggleButtonGroup, + ToggleButtonGroupStyle, ToggleButtonSimple, ToggleState, prelude::*, }; -use crate::{ImportCursorSettings, ImportVsCodeSettings, SettingsImportState}; +use crate::{ImportCursorSettings, ImportVsCodeSettings}; fn read_show_mini_map(cx: &App) -> ShowMinimap { editor::EditorSettings::get_global(cx).minimap.show @@ -35,11 +28,6 @@ fn write_show_mini_map(show: ShowMinimap, cx: &mut App) { EditorSettings::override_global(curr_settings, cx); update_settings_file::(fs, cx, move |editor_settings, _| { - telemetry::event!( - "Welcome Minimap Clicked", - from = editor_settings.minimap.unwrap_or_default(), - to = show - ); editor_settings.minimap.get_or_insert_default().show = Some(show); }); } @@ -76,7 +64,7 @@ fn read_git_blame(cx: &App) -> bool { ProjectSettings::get_global(cx).git.inline_blame_enabled() } -fn write_git_blame(enabled: bool, cx: &mut App) { +fn set_git_blame(enabled: bool, cx: &mut App) { let fs = ::global(cx); let mut curr_settings = ProjectSettings::get_global(cx).clone(); @@ -100,12 +88,6 @@ fn write_ui_font_family(font: SharedString, cx: &mut App) { let fs = ::global(cx); update_settings_file::(fs, cx, move |theme_settings, _| { - telemetry::event!( - "Welcome Font Changed", - type = "ui font", - old = theme_settings.ui_font_family, - new = font - ); theme_settings.ui_font_family = Some(FontFamilyName(font.into())); }); } @@ -130,135 +112,11 @@ fn write_buffer_font_family(font_family: SharedString, cx: &mut App) { let fs = ::global(cx); update_settings_file::(fs, cx, move |theme_settings, _| { - telemetry::event!( - "Welcome Font Changed", - type = "editor font", - old = theme_settings.buffer_font_family, - new = font_family - ); - theme_settings.buffer_font_family = Some(FontFamilyName(font_family.into())); }); } -fn read_font_ligatures(cx: &App) -> bool { - ThemeSettings::get_global(cx) - .buffer_font - .features - .is_calt_enabled() - .unwrap_or(true) -} - -fn write_font_ligatures(enabled: bool, cx: &mut App) { - let fs = ::global(cx); - let bit = if enabled { 1 } else { 0 }; - - update_settings_file::(fs, cx, move |theme_settings, _| { - let mut features = theme_settings - .buffer_font_features - .as_mut() - .map(|features| features.tag_value_list().to_vec()) - .unwrap_or_default(); - - if let Some(calt_index) = features.iter().position(|(tag, _)| tag == "calt") { - features[calt_index].1 = bit; - } else { - features.push(("calt".into(), bit)); - } - - theme_settings.buffer_font_features = Some(FontFeatures(Arc::new(features))); - }); -} - -fn read_format_on_save(cx: &App) -> bool { - match AllLanguageSettings::get_global(cx).defaults.format_on_save { - FormatOnSave::On | FormatOnSave::List(_) => true, - FormatOnSave::Off => false, - } -} - -fn write_format_on_save(format_on_save: bool, cx: &mut App) { - let fs = ::global(cx); - - update_settings_file::(fs, cx, move |language_settings, _| { - language_settings.defaults.format_on_save = Some(match format_on_save { - true => FormatOnSave::On, - false => FormatOnSave::Off, - }); - }); -} - -fn render_setting_import_button( - tab_index: isize, - label: SharedString, - icon_name: IconName, - action: &dyn Action, - imported: bool, -) -> impl IntoElement { - let action = action.boxed_clone(); - h_flex().w_full().child( - ButtonLike::new(label.clone()) - .full_width() - .style(ButtonStyle::Outlined) - .size(ButtonSize::Large) - .tab_index(tab_index) - .child( - h_flex() - .w_full() - .justify_between() - .child( - h_flex() - .gap_1p5() - .px_1() - .child( - Icon::new(icon_name) - .color(Color::Muted) - .size(IconSize::XSmall), - ) - .child(Label::new(label.clone())), - ) - .when(imported, |this| { - this.child( - h_flex() - .gap_1p5() - .child( - Icon::new(IconName::Check) - .color(Color::Success) - .size(IconSize::XSmall), - ) - .child(Label::new("Imported").size(LabelSize::Small)), - ) - }), - ) - .on_click(move |_, window, cx| { - telemetry::event!("Welcome Import Settings", import_source = label,); - window.dispatch_action(action.boxed_clone(), cx); - }), - ) -} - -fn render_import_settings_section(tab_index: &mut isize, cx: &App) -> impl IntoElement { - let import_state = SettingsImportState::global(cx); - let imports: [(SharedString, IconName, &dyn Action, bool); 2] = [ - ( - "VS Code".into(), - IconName::EditorVsCode, - &ImportVsCodeSettings { skip_prompt: false }, - import_state.vscode, - ), - ( - "Cursor".into(), - IconName::EditorCursor, - &ImportCursorSettings { skip_prompt: false }, - import_state.cursor, - ), - ]; - - let [vscode, cursor] = imports.map(|(label, icon_name, action, imported)| { - *tab_index += 1; - render_setting_import_button(*tab_index - 1, label, icon_name, action, imported) - }); - +fn render_import_settings_section() -> impl IntoElement { v_flex() .gap_4() .child( @@ -269,35 +127,71 @@ fn render_import_settings_section(tab_index: &mut isize, cx: &App) -> impl IntoE .color(Color::Muted), ), ) - .child(h_flex().w_full().gap_4().child(vscode).child(cursor)) + .child( + h_flex() + .w_full() + .gap_4() + .child( + h_flex().w_full().child( + ButtonLike::new("import_vs_code") + .full_width() + .style(ButtonStyle::Outlined) + .size(ButtonSize::Large) + .child( + h_flex() + .w_full() + .gap_1p5() + .px_1() + .child( + Icon::new(IconName::Sparkle) + .color(Color::Muted) + .size(IconSize::XSmall), + ) + .child(Label::new("VS Code")), + ) + .on_click(|_, window, cx| { + window.dispatch_action( + ImportVsCodeSettings::default().boxed_clone(), + cx, + ) + }), + ), + ) + .child( + h_flex().w_full().child( + ButtonLike::new("import_cursor") + .full_width() + .style(ButtonStyle::Outlined) + .size(ButtonSize::Large) + .child( + h_flex() + .w_full() + .gap_1p5() + .px_1() + .child( + Icon::new(IconName::Sparkle) + .color(Color::Muted) + .size(IconSize::XSmall), + ) + .child(Label::new("Cursor")), + ) + .on_click(|_, window, cx| { + window.dispatch_action( + ImportCursorSettings::default().boxed_clone(), + cx, + ) + }), + ), + ), + ) } -fn render_font_customization_section( - tab_index: &mut isize, - window: &mut Window, - cx: &mut App, -) -> impl IntoElement { +fn render_font_customization_section(window: &mut Window, cx: &mut App) -> impl IntoElement { let theme_settings = ThemeSettings::get_global(cx); let ui_font_size = theme_settings.ui_font_size(cx); - let ui_font_family = theme_settings.ui_font.family.clone(); - let buffer_font_family = theme_settings.buffer_font.family.clone(); + let font_family = theme_settings.buffer_font.family.clone(); let buffer_font_size = theme_settings.buffer_font_size(cx); - let ui_font_picker = - cx.new(|cx| font_picker(ui_font_family.clone(), write_ui_font_family, window, cx)); - - let buffer_font_picker = cx.new(|cx| { - font_picker( - buffer_font_family.clone(), - write_buffer_font_family, - window, - cx, - ) - }); - - let ui_font_handle = ui::PopoverMenuHandle::default(); - let buffer_font_handle = ui::PopoverMenuHandle::default(); - h_flex() .w_full() .gap_4() @@ -312,39 +206,34 @@ fn render_font_customization_section( .justify_between() .gap_2() .child( - PopoverMenu::new("ui-font-picker") - .menu({ - let ui_font_picker = ui_font_picker; - move |_window, _cx| Some(ui_font_picker.clone()) - }) - .trigger( - ButtonLike::new("ui-font-family-button") - .style(ButtonStyle::Outlined) - .size(ButtonSize::Medium) - .full_width() - .tab_index({ - *tab_index += 1; - *tab_index - 1 - }) - .child( - h_flex() - .w_full() - .justify_between() - .child(Label::new(ui_font_family)) - .child( - Icon::new(IconName::ChevronUpDown) - .color(Color::Muted) - .size(IconSize::XSmall), - ), - ), - ) - .full_width(true) - .anchor(gpui::Corner::TopLeft) - .offset(gpui::Point { - x: px(0.0), - y: px(4.0), - }) - .with_handle(ui_font_handle), + DropdownMenu::new( + "ui-font-family", + theme_settings.ui_font.family.clone(), + ContextMenu::build(window, cx, |mut menu, _, cx| { + let font_family_cache = FontFamilyCache::global(cx); + + for font_name in font_family_cache.list_font_families(cx) { + menu = menu.custom_entry( + { + let font_name = font_name.clone(); + move |_window, _cx| { + Label::new(font_name.clone()).into_any_element() + } + }, + { + let font_name = font_name.clone(); + move |_window, cx| { + write_ui_font_family(font_name.clone(), cx); + } + }, + ) + } + + menu + }), + ) + .style(ui::DropdownStyle::Outlined) + .full_width(true), ) .child( NumericStepper::new( @@ -357,11 +246,7 @@ fn render_font_customization_section( write_ui_font_size(ui_font_size + px(1.), cx); }, ) - .style(ui::NumericStepperStyle::Outlined) - .tab_index({ - *tab_index += 2; - *tab_index - 2 - }), + .style(ui::NumericStepperStyle::Outlined), ), ), ) @@ -376,39 +261,34 @@ fn render_font_customization_section( .justify_between() .gap_2() .child( - PopoverMenu::new("buffer-font-picker") - .menu({ - let buffer_font_picker = buffer_font_picker; - move |_window, _cx| Some(buffer_font_picker.clone()) - }) - .trigger( - ButtonLike::new("buffer-font-family-button") - .style(ButtonStyle::Outlined) - .size(ButtonSize::Medium) - .full_width() - .tab_index({ - *tab_index += 1; - *tab_index - 1 - }) - .child( - h_flex() - .w_full() - .justify_between() - .child(Label::new(buffer_font_family)) - .child( - Icon::new(IconName::ChevronUpDown) - .color(Color::Muted) - .size(IconSize::XSmall), - ), - ), - ) - .full_width(true) - .anchor(gpui::Corner::TopLeft) - .offset(gpui::Point { - x: px(0.0), - y: px(4.0), - }) - .with_handle(buffer_font_handle), + DropdownMenu::new( + "buffer-font-family", + font_family, + ContextMenu::build(window, cx, |mut menu, _, cx| { + let font_family_cache = FontFamilyCache::global(cx); + + for font_name in font_family_cache.list_font_families(cx) { + menu = menu.custom_entry( + { + let font_name = font_name.clone(); + move |_window, _cx| { + Label::new(font_name.clone()).into_any_element() + } + }, + { + let font_name = font_name.clone(); + move |_window, cx| { + write_buffer_font_family(font_name.clone(), cx); + } + }, + ) + } + + menu + }), + ) + .style(ui::DropdownStyle::Outlined) + .full_width(true), ) .child( NumericStepper::new( @@ -421,307 +301,23 @@ fn render_font_customization_section( write_buffer_font_size(buffer_font_size + px(1.), cx); }, ) - .style(ui::NumericStepperStyle::Outlined) - .tab_index({ - *tab_index += 2; - *tab_index - 2 - }), + .style(ui::NumericStepperStyle::Outlined), ), ), ) } -type FontPicker = Picker; - -pub struct FontPickerDelegate { - fonts: Vec, - filtered_fonts: Vec, - selected_index: usize, - current_font: SharedString, - on_font_changed: Arc, -} - -impl FontPickerDelegate { - fn new( - current_font: SharedString, - on_font_changed: impl Fn(SharedString, &mut App) + 'static, - cx: &mut Context, - ) -> Self { - let font_family_cache = FontFamilyCache::global(cx); - - let fonts: Vec = font_family_cache - .list_font_families(cx) - .into_iter() - .collect(); - - let selected_index = fonts - .iter() - .position(|font| *font == current_font) - .unwrap_or(0); - - Self { - fonts: fonts.clone(), - filtered_fonts: fonts - .iter() - .enumerate() - .map(|(index, font)| StringMatch { - candidate_id: index, - string: font.to_string(), - positions: Vec::new(), - score: 0.0, - }) - .collect(), - selected_index, - current_font, - on_font_changed: Arc::new(on_font_changed), - } - } -} - -impl PickerDelegate for FontPickerDelegate { - type ListItem = AnyElement; - - fn match_count(&self) -> usize { - self.filtered_fonts.len() - } - - fn selected_index(&self) -> usize { - self.selected_index - } - - fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context) { - self.selected_index = ix.min(self.filtered_fonts.len().saturating_sub(1)); - cx.notify(); - } - - fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - "Search fonts…".into() - } - - fn update_matches( - &mut self, - query: String, - _window: &mut Window, - cx: &mut Context, - ) -> Task<()> { - let fonts = self.fonts.clone(); - let current_font = self.current_font.clone(); - - let matches: Vec = if query.is_empty() { - fonts - .iter() - .enumerate() - .map(|(index, font)| StringMatch { - candidate_id: index, - string: font.to_string(), - positions: Vec::new(), - score: 0.0, - }) - .collect() - } else { - let _candidates: Vec = fonts - .iter() - .enumerate() - .map(|(id, font)| StringMatchCandidate::new(id, font.as_ref())) - .collect(); - - fonts - .iter() - .enumerate() - .filter(|(_, font)| font.to_lowercase().contains(&query.to_lowercase())) - .map(|(index, font)| StringMatch { - candidate_id: index, - string: font.to_string(), - positions: Vec::new(), - score: 0.0, - }) - .collect() - }; - - let selected_index = if query.is_empty() { - fonts - .iter() - .position(|font| *font == current_font) - .unwrap_or(0) - } else { - matches - .iter() - .position(|m| fonts[m.candidate_id] == current_font) - .unwrap_or(0) - }; - - self.filtered_fonts = matches; - self.selected_index = selected_index; - cx.notify(); - - Task::ready(()) - } - - fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context) { - if let Some(font_match) = self.filtered_fonts.get(self.selected_index) { - let font = font_match.string.clone(); - (self.on_font_changed)(font.into(), cx); - } - } - - fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context) {} - - fn render_match( - &self, - ix: usize, - selected: bool, - _window: &mut Window, - _cx: &mut Context, - ) -> Option { - let font_match = self.filtered_fonts.get(ix)?; - - Some( - ListItem::new(ix) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .toggle_state(selected) - .child(Label::new(font_match.string.clone())) - .into_any_element(), - ) - } -} - -fn font_picker( - current_font: SharedString, - on_font_changed: impl Fn(SharedString, &mut App) + 'static, - window: &mut Window, - cx: &mut Context, -) -> FontPicker { - let delegate = FontPickerDelegate::new(current_font, on_font_changed, cx); - - Picker::uniform_list(delegate, window, cx) - .show_scrollbar(true) - .width(rems_from_px(210.)) - .max_height(Some(rems(20.).into())) -} - -fn render_popular_settings_section( - tab_index: &mut isize, - window: &mut Window, - cx: &mut App, -) -> impl IntoElement { - const LIGATURE_TOOLTIP: &str = - "Font ligatures combine two characters into one. For example, turning != into ≠."; - +fn render_popular_settings_section(window: &mut Window, cx: &mut App) -> impl IntoElement { v_flex() - .pt_6() - .gap_4() - .border_t_1() - .border_color(cx.theme().colors().border_variant.opacity(0.5)) - .child(Label::new("Popular Settings").size(LabelSize::Large)) - .child(render_font_customization_section(tab_index, window, cx)) - .child( - SwitchField::new( - "onboarding-font-ligatures", - "Font Ligatures", - Some("Combine text characters into their associated symbols.".into()), - if read_font_ligatures(cx) { - ui::ToggleState::Selected - } else { - ui::ToggleState::Unselected - }, - |toggle_state, _, cx| { - let enabled = toggle_state == &ToggleState::Selected; - telemetry::event!( - "Welcome Font Ligature", - options = if enabled { "on" } else { "off" }, - ); - - write_font_ligatures(enabled, cx); - }, - ) - .tab_index({ - *tab_index += 1; - *tab_index - 1 - }) - .tooltip(Tooltip::text(LIGATURE_TOOLTIP)), - ) - .child( - SwitchField::new( - "onboarding-format-on-save", - "Format on Save", - Some("Format code automatically when saving.".into()), - if read_format_on_save(cx) { - ui::ToggleState::Selected - } else { - ui::ToggleState::Unselected - }, - |toggle_state, _, cx| { - let enabled = toggle_state == &ToggleState::Selected; - telemetry::event!( - "Welcome Format On Save Changed", - options = if enabled { "on" } else { "off" }, - ); - - write_format_on_save(enabled, cx); - }, - ) - .tab_index({ - *tab_index += 1; - *tab_index - 1 - }), - ) - .child( - SwitchField::new( - "onboarding-enable-inlay-hints", - "Inlay Hints", - Some("See parameter names for function and method calls inline.".into()), - if read_inlay_hints(cx) { - ui::ToggleState::Selected - } else { - ui::ToggleState::Unselected - }, - |toggle_state, _, cx| { - let enabled = toggle_state == &ToggleState::Selected; - telemetry::event!( - "Welcome Inlay Hints Changed", - options = if enabled { "on" } else { "off" }, - ); - - write_inlay_hints(enabled, cx); - }, - ) - .tab_index({ - *tab_index += 1; - *tab_index - 1 - }), - ) - .child( - SwitchField::new( - "onboarding-git-blame-switch", - "Inline Git Blame", - Some("See who committed each line on a given file.".into()), - if read_git_blame(cx) { - ui::ToggleState::Selected - } else { - ui::ToggleState::Unselected - }, - |toggle_state, _, cx| { - let enabled = toggle_state == &ToggleState::Selected; - telemetry::event!( - "Welcome Git Blame Changed", - options = if enabled { "on" } else { "off" }, - ); - - write_git_blame(enabled, cx); - }, - ) - .tab_index({ - *tab_index += 1; - *tab_index - 1 - }), - ) + .gap_5() + .child(Label::new("Popular Settings").size(LabelSize::Large).mt_8()) + .child(render_font_customization_section(window, cx)) .child( h_flex() .items_start() .justify_between() .child( - v_flex().child(Label::new("Minimap")).child( + v_flex().child(Label::new("Mini Map")).child( Label::new("See a high-level overview of your source code.") .color(Color::Muted), ), @@ -732,10 +328,7 @@ fn render_popular_settings_section( [ ToggleButtonSimple::new("Auto", |_, _, cx| { write_show_mini_map(ShowMinimap::Auto, cx); - }) - .tooltip(Tooltip::text( - "Show the minimap if the editor's scrollbar is visible.", - )), + }), ToggleButtonSimple::new("Always", |_, _, cx| { write_show_mini_map(ShowMinimap::Always, cx); }), @@ -749,17 +342,41 @@ fn render_popular_settings_section( ShowMinimap::Always => 1, ShowMinimap::Never => 2, }) - .tab_index(tab_index) .style(ToggleButtonGroupStyle::Outlined) - .width(ui::rems_from_px(3. * 64.)), + .button_width(ui::rems_from_px(64.)), ), ) + .child(SwitchField::new( + "onboarding-enable-inlay-hints", + "Inlay Hints", + "See parameter names for function and method calls inline.", + if read_inlay_hints(cx) { + ui::ToggleState::Selected + } else { + ui::ToggleState::Unselected + }, + |toggle_state, _, cx| { + write_inlay_hints(toggle_state == &ToggleState::Selected, cx); + }, + )) + .child(SwitchField::new( + "onboarding-git-blame-switch", + "Git Blame", + "See who committed each line on a given file.", + if read_git_blame(cx) { + ui::ToggleState::Selected + } else { + ui::ToggleState::Unselected + }, + |toggle_state, _, cx| { + set_git_blame(toggle_state == &ToggleState::Selected, cx); + }, + )) } pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl IntoElement { - let mut tab_index = 0; v_flex() - .gap_6() - .child(render_import_settings_section(&mut tab_index, cx)) - .child(render_popular_settings_section(&mut tab_index, window, cx)) + .gap_4() + .child(render_import_settings_section()) + .child(render_popular_settings_section(window, cx)) } diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index 873dd63201..6496c09e79 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -1,38 +1,41 @@ -pub use crate::welcome::ShowWelcome; -use crate::{multibuffer_hint::MultibufferHint, welcome::WelcomePage}; -use client::{Client, UserStore, zed_urls}; +use crate::welcome::{ShowWelcome, WelcomePage}; +use client::{Client, UserStore}; +use command_palette_hooks::CommandPaletteFilter; use db::kvp::KEY_VALUE_STORE; +use feature_flags::{FeatureFlag, FeatureFlagViewExt as _}; use fs::Fs; use gpui::{ Action, AnyElement, App, AppContext, AsyncWindowContext, Context, Entity, EventEmitter, - FocusHandle, Focusable, Global, IntoElement, KeyContext, Render, SharedString, Subscription, - Task, WeakEntity, Window, actions, + FocusHandle, Focusable, IntoElement, Render, SharedString, Subscription, Task, WeakEntity, + Window, actions, }; -use notifications::status_toast::{StatusToast, ToastIcon}; use schemars::JsonSchema; use serde::Deserialize; use settings::{SettingsStore, VsCodeSettingsSource}; use std::sync::Arc; use ui::{ - Avatar, ButtonLike, FluentBuilder, Headline, KeyBinding, ParentElement as _, - StatefulInteractiveElement, Vector, VectorName, prelude::*, rems_from_px, + Avatar, FluentBuilder, Headline, KeyBinding, ParentElement as _, StatefulInteractiveElement, + Vector, VectorName, prelude::*, rems_from_px, }; use workspace::{ AppState, Workspace, WorkspaceId, dock::DockPosition, item::{Item, ItemEvent}, notifications::NotifyResultExt as _, - open_new, register_serializable_item, with_active_or_new_workspace, + open_new, with_active_or_new_workspace, }; -mod ai_setup_page; -mod base_keymap_picker; mod basics_page; mod editing_page; -pub mod multibuffer_hint; mod theme_preview; mod welcome; +pub struct OnBoardingFeatureFlag {} + +impl FeatureFlag for OnBoardingFeatureFlag { + const NAME: &'static str = "onboarding"; +} + /// Imports settings from Visual Studio Code. #[derive(Copy, Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, Action)] #[action(namespace = zed)] @@ -52,7 +55,6 @@ pub struct ImportCursorSettings { } pub const FIRST_OPEN: &str = "first_open"; -pub const DOCS_URL: &str = "https://zed.dev/docs/"; actions!( zed, @@ -62,33 +64,7 @@ actions!( ] ); -actions!( - onboarding, - [ - /// Activates the Basics page. - ActivateBasicsPage, - /// Activates the Editing page. - ActivateEditingPage, - /// Activates the AI Setup page. - ActivateAISetupPage, - /// Finish the onboarding process. - Finish, - /// Sign in while in the onboarding flow. - SignIn, - /// Open the user account in zed.dev while in the onboarding flow. - OpenAccount, - /// Resets the welcome screen hints to their initial state. - ResetHints - ] -); - pub fn init(cx: &mut App) { - cx.observe_new(|workspace: &mut Workspace, _, _cx| { - workspace - .register_action(|_workspace, _: &ResetHints, _, cx| MultibufferHint::set_count(0, cx)); - }) - .detach(); - cx.on_action(|_: &OpenOnboarding, cx| { with_active_or_new_workspace(cx, |workspace, window, cx| { workspace @@ -102,7 +78,11 @@ pub fn init(cx: &mut App) { if let Some(existing) = existing { workspace.activate_item(&existing, true, true, window, cx); } else { - let settings_page = Onboarding::new(workspace, cx); + let settings_page = Onboarding::new( + workspace.weak_handle(), + workspace.user_store().clone(), + cx, + ); workspace.add_item_to_active_pane( Box::new(settings_page), None, @@ -148,12 +128,9 @@ pub fn init(cx: &mut App) { let fs = ::global(cx); let action = *action; - let workspace = cx.weak_entity(); - window .spawn(cx, async move |cx: &mut AsyncWindowContext| { handle_import_vscode_settings( - workspace, VsCodeSettingsSource::VsCode, action.skip_prompt, fs, @@ -168,12 +145,9 @@ pub fn init(cx: &mut App) { let fs = ::global(cx); let action = *action; - let workspace = cx.weak_entity(); - window .spawn(cx, async move |cx: &mut AsyncWindowContext| { handle_import_vscode_settings( - workspace, VsCodeSettingsSource::Cursor, action.skip_prompt, fs, @@ -186,14 +160,37 @@ pub fn init(cx: &mut App) { }) .detach(); - base_keymap_picker::init(cx); + cx.observe_new::(|_, window, cx| { + let Some(window) = window else { + return; + }; - register_serializable_item::(cx); - register_serializable_item::(cx); + let onboarding_actions = [ + std::any::TypeId::of::(), + std::any::TypeId::of::(), + ]; + + CommandPaletteFilter::update_global(cx, |filter, _cx| { + filter.hide_action_types(&onboarding_actions); + }); + + cx.observe_flag::(window, move |is_enabled, _, _, cx| { + if is_enabled { + CommandPaletteFilter::update_global(cx, |filter, _cx| { + filter.show_action_types(onboarding_actions.iter()); + }); + } else { + CommandPaletteFilter::update_global(cx, |filter, _cx| { + filter.hide_action_types(&onboarding_actions); + }); + } + }) + .detach(); + }) + .detach(); } pub fn show_onboarding_view(app_state: Arc, cx: &mut App) -> Task> { - telemetry::event!("Onboarding Page Opened"); open_new( Default::default(), app_state, @@ -201,7 +198,8 @@ pub fn show_onboarding_view(app_state: Arc, cx: &mut App) -> Task &'static str { - match self { - SelectedPage::Basics => "Basics", - SelectedPage::Editing => "Editing", - SelectedPage::AiSetup => "AI Setup", - } - } -} - struct Onboarding { workspace: WeakEntity, focus_handle: FocusHandle, @@ -241,108 +229,84 @@ struct Onboarding { } impl Onboarding { - fn new(workspace: &Workspace, cx: &mut App) -> Entity { + fn new( + workspace: WeakEntity, + user_store: Entity, + cx: &mut App, + ) -> Entity { cx.new(|cx| Self { - workspace: workspace.weak_handle(), + workspace, + user_store, focus_handle: cx.focus_handle(), selected_page: SelectedPage::Basics, - user_store: workspace.user_store().clone(), _settings_subscription: cx.observe_global::(move |_, cx| cx.notify()), }) } - fn set_page( + fn render_nav_button( &mut self, page: SelectedPage, - clicked: Option<&'static str>, + _: &mut Window, cx: &mut Context, - ) { - if let Some(click) = clicked { - telemetry::event!( - "Welcome Tab Clicked", - from = self.selected_page.name(), - to = page.name(), - clicked = click, - ); - } + ) -> impl IntoElement { + let text = match page { + SelectedPage::Basics => "Basics", + SelectedPage::Editing => "Editing", + SelectedPage::AiSetup => "AI Setup", + }; - self.selected_page = page; - cx.notify(); - cx.emit(ItemEvent::UpdateTab); - } + let binding = match page { + SelectedPage::Basics => { + KeyBinding::new(vec![gpui::Keystroke::parse("cmd-1").unwrap()], cx) + .map(|kb| kb.size(rems_from_px(12.))) + } + SelectedPage::Editing => { + KeyBinding::new(vec![gpui::Keystroke::parse("cmd-2").unwrap()], cx) + .map(|kb| kb.size(rems_from_px(12.))) + } + SelectedPage::AiSetup => { + KeyBinding::new(vec![gpui::Keystroke::parse("cmd-3").unwrap()], cx) + .map(|kb| kb.size(rems_from_px(12.))) + } + }; - fn render_nav_buttons( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> [impl IntoElement; 3] { - let pages = [ - SelectedPage::Basics, - SelectedPage::Editing, - SelectedPage::AiSetup, - ]; + let selected = self.selected_page == page; - let text = ["Basics", "Editing", "AI Setup"]; - - let actions: [&dyn Action; 3] = [ - &ActivateBasicsPage, - &ActivateEditingPage, - &ActivateAISetupPage, - ]; - - let mut binding = actions.map(|action| { - KeyBinding::for_action_in(action, &self.focus_handle, window, cx) - .map(|kb| kb.size(rems_from_px(12.))) - }); - - pages.map(|page| { - let i = page as usize; - let selected = self.selected_page == page; - h_flex() - .id(text[i]) - .relative() - .w_full() - .gap_2() - .px_2() - .py_0p5() - .justify_between() - .rounded_sm() - .when(selected, |this| { - this.child( - div() - .h_4() - .w_px() - .bg(cx.theme().colors().text_accent) - .absolute() - .left_0(), - ) - }) - .hover(|style| style.bg(cx.theme().colors().element_hover)) - .child(Label::new(text[i]).map(|this| { - if selected { - this.color(Color::Default) - } else { - this.color(Color::Muted) - } - })) - .child(binding[i].take().map_or( - gpui::Empty.into_any_element(), - IntoElement::into_any_element, - )) - .on_click(cx.listener(move |this, click_event, _, cx| { - let click = match click_event { - gpui::ClickEvent::Mouse(_) => "mouse", - gpui::ClickEvent::Keyboard(_) => "keyboard", - }; - - this.set_page(page, Some(click), cx); - })) - }) + h_flex() + .id(text) + .relative() + .w_full() + .gap_2() + .px_2() + .py_0p5() + .justify_between() + .rounded_sm() + .when(selected, |this| { + this.child( + div() + .h_4() + .w_px() + .bg(cx.theme().colors().text_accent) + .absolute() + .left_0(), + ) + }) + .hover(|style| style.bg(cx.theme().colors().element_hover)) + .child(Label::new(text).map(|this| { + if selected { + this.color(Color::Default) + } else { + this.color(Color::Muted) + } + })) + .child(binding) + .on_click(cx.listener(move |this, _, _, cx| { + this.selected_page = page; + cx.notify(); + })) } fn render_nav(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let ai_setup_page = matches!(self.selected_page, SelectedPage::AiSetup); - v_flex() .h_full() .w(rems_from_px(220.)) @@ -379,189 +343,73 @@ impl Onboarding { .border_y_1() .border_color(cx.theme().colors().border_variant.opacity(0.5)) .gap_1() - .children(self.render_nav_buttons(window, cx)), + .children([ + self.render_nav_button(SelectedPage::Basics, window, cx) + .into_element(), + self.render_nav_button(SelectedPage::Editing, window, cx) + .into_element(), + self.render_nav_button(SelectedPage::AiSetup, window, cx) + .into_element(), + ]), ) - .map(|this| { - let keybinding = KeyBinding::for_action_in( - &Finish, - &self.focus_handle, - window, - cx, - ) - .map(|kb| kb.size(rems_from_px(12.))); - - if ai_setup_page { - this.child( - ButtonLike::new("start_building") - .style(ButtonStyle::Outlined) - .size(ButtonSize::Medium) - .child( - h_flex() - .ml_1() - .w_full() - .justify_between() - .child(Label::new("Start Building")) - .children(keybinding), - ) - .on_click(|_, window, cx| { - window.dispatch_action(Finish.boxed_clone(), cx); - }), - ) - } else { - this.child( - ButtonLike::new("skip_all") - .size(ButtonSize::Medium) - .child( - h_flex() - .ml_1() - .w_full() - .justify_between() - .child( - Label::new("Skip All").color(Color::Muted), - ) - .children(keybinding), - ) - .on_click(|_, window, cx| { - window.dispatch_action(Finish.boxed_clone(), cx); - }), - ) - } - }), + .child(Button::new("skip_all", "Skip All")), ), ) .child( if let Some(user) = self.user_store.read(cx).current_user() { - v_flex() - .gap_1() - .child( - h_flex() - .ml_2() - .gap_2() - .max_w_full() - .w_full() - .child(Avatar::new(user.avatar_uri.clone())) - .child(Label::new(user.github_login.clone()).truncate()), - ) - .child( - ButtonLike::new("open_account") - .size(ButtonSize::Medium) - .child( - h_flex() - .ml_1() - .w_full() - .justify_between() - .child(Label::new("Open Account")) - .children( - KeyBinding::for_action_in( - &OpenAccount, - &self.focus_handle, - window, - cx, - ) - .map(|kb| kb.size(rems_from_px(12.))), - ), - ) - .on_click(|_, window, cx| { - window.dispatch_action(OpenAccount.boxed_clone(), cx); - }), - ) + h_flex() + .gap_2() + .child(Avatar::new(user.avatar_uri.clone())) + .child(Label::new(user.github_login.clone())) .into_any_element() } else { Button::new("sign_in", "Sign In") - .full_width() .style(ButtonStyle::Outlined) - .size(ButtonSize::Medium) - .key_binding( - KeyBinding::for_action_in(&SignIn, &self.focus_handle, window, cx) - .map(|kb| kb.size(rems_from_px(12.))), - ) + .full_width() .on_click(|_, window, cx| { - window.dispatch_action(SignIn.boxed_clone(), cx); + let client = Client::global(cx); + window + .spawn(cx, async move |cx| { + client + .authenticate_and_connect(true, &cx) + .await + .into_response() + .notify_async_err(cx); + }) + .detach(); }) .into_any_element() }, ) } - fn on_finish(_: &Finish, _: &mut Window, cx: &mut App) { - telemetry::event!("Welcome Skip Clicked"); - go_to_welcome_page(cx); - } - - fn handle_sign_in(_: &SignIn, window: &mut Window, cx: &mut App) { - let client = Client::global(cx); - - window - .spawn(cx, async move |cx| { - client - .sign_in_with_optional_connect(true, cx) - .await - .notify_async_err(cx); - }) - .detach(); - } - - fn handle_open_account(_: &OpenAccount, _: &mut Window, cx: &mut App) { - cx.open_url(&zed_urls::account_url(cx)) - } - fn render_page(&mut self, window: &mut Window, cx: &mut Context) -> AnyElement { - let client = Client::global(cx); - match self.selected_page { - SelectedPage::Basics => crate::basics_page::render_basics_page(cx).into_any_element(), + SelectedPage::Basics => { + crate::basics_page::render_basics_page(window, cx).into_any_element() + } SelectedPage::Editing => { crate::editing_page::render_editing_page(window, cx).into_any_element() } - SelectedPage::AiSetup => crate::ai_setup_page::render_ai_setup_page( - self.workspace.clone(), - self.user_store.clone(), - client, - window, - cx, - ) - .into_any_element(), + SelectedPage::AiSetup => self.render_ai_setup_page(window, cx).into_any_element(), } } + + fn render_ai_setup_page(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + div().child("ai setup page") + } } impl Render for Onboarding { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { h_flex() .image_cache(gpui::retain_all("onboarding-page")) - .key_context({ - let mut ctx = KeyContext::new_with_defaults(); - ctx.add("Onboarding"); - ctx.add("menu"); - ctx - }) - .track_focus(&self.focus_handle) + .key_context("onboarding-page") .size_full() .bg(cx.theme().colors().editor_background) - .on_action(Self::on_finish) - .on_action(Self::handle_sign_in) - .on_action(Self::handle_open_account) - .on_action(cx.listener(|this, _: &ActivateBasicsPage, _, cx| { - this.set_page(SelectedPage::Basics, Some("action"), cx); - })) - .on_action(cx.listener(|this, _: &ActivateEditingPage, _, cx| { - this.set_page(SelectedPage::Editing, Some("action"), cx); - })) - .on_action(cx.listener(|this, _: &ActivateAISetupPage, _, cx| { - this.set_page(SelectedPage::AiSetup, Some("action"), cx); - })) - .on_action(cx.listener(|_, _: &menu::SelectNext, window, cx| { - window.focus_next(); - cx.notify(); - })) - .on_action(cx.listener(|_, _: &menu::SelectPrevious, window, cx| { - window.focus_prev(); - cx.notify(); - })) .child( h_flex() .max_w(rems_from_px(1100.)) - .max_h(rems_from_px(850.)) .size_full() .m_auto() .py_20() @@ -570,15 +418,11 @@ impl Render for Onboarding { .gap_12() .child(self.render_nav(window, cx)) .child( - v_flex() - .id("page-content") - .size_full() - .max_w_full() - .min_w_0() + div() .pl_12() .border_l_1() .border_color(cx.theme().colors().border_variant.opacity(0.5)) - .overflow_y_scroll() + .size_full() .child(self.render_page(window, cx)), ), ) @@ -614,13 +458,11 @@ impl Item for Onboarding { _: &mut Window, cx: &mut Context, ) -> Option> { - Some(cx.new(|cx| Onboarding { - workspace: self.workspace.clone(), - user_store: self.user_store.clone(), - selected_page: self.selected_page, - focus_handle: cx.focus_handle(), - _settings_subscription: cx.observe_global::(move |_, cx| cx.notify()), - })) + Some(Onboarding::new( + self.workspace.clone(), + self.user_store.clone(), + cx, + )) } fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) { @@ -628,42 +470,7 @@ impl Item for Onboarding { } } -fn go_to_welcome_page(cx: &mut App) { - with_active_or_new_workspace(cx, |workspace, window, cx| { - let Some((onboarding_id, onboarding_idx)) = workspace - .active_pane() - .read(cx) - .items() - .enumerate() - .find_map(|(idx, item)| { - let _ = item.downcast::()?; - Some((item.item_id(), idx)) - }) - else { - return; - }; - - workspace.active_pane().update(cx, |pane, cx| { - // Get the index here to get around the borrow checker - let idx = pane.items().enumerate().find_map(|(idx, item)| { - let _ = item.downcast::()?; - Some(idx) - }); - - if let Some(idx) = idx { - pane.activate_item(idx, true, true, window, cx); - } else { - let item = Box::new(WelcomePage::new(window, cx)); - pane.add_item(item, true, true, Some(onboarding_idx), window, cx); - } - - pane.remove_item(onboarding_id, false, false, window, cx); - }); - }); -} - pub async fn handle_import_vscode_settings( - workspace: WeakEntity, source: VsCodeSettingsSource, skip_prompt: bool, fs: Arc, @@ -704,200 +511,12 @@ pub async fn handle_import_vscode_settings( } }; - let Ok(result_channel) = cx.update(|_, cx| { + cx.update(|_, cx| { let source = vscode_settings.source; let path = vscode_settings.path.clone(); - let result_channel = cx - .global::() + cx.global::() .import_vscode_settings(fs, vscode_settings); zlog::info!("Imported {source} settings from {}", path.display()); - result_channel - }) else { - return; - }; - - let result = result_channel.await; - workspace - .update_in(cx, |workspace, _, cx| match result { - Ok(_) => { - let confirmation_toast = StatusToast::new( - format!("Your {} settings were successfully imported.", source), - cx, - |this, _| { - this.icon(ToastIcon::new(IconName::Check).color(Color::Success)) - .dismiss_button(true) - }, - ); - SettingsImportState::update(cx, |state, _| match source { - VsCodeSettingsSource::VsCode => { - state.vscode = true; - } - VsCodeSettingsSource::Cursor => { - state.cursor = true; - } - }); - workspace.toggle_status_toast(confirmation_toast, cx); - } - Err(_) => { - let error_toast = StatusToast::new( - "Failed to import settings. See log for details", - cx, - |this, _| { - this.icon(ToastIcon::new(IconName::Close).color(Color::Error)) - .action("Open Log", |window, cx| { - window.dispatch_action(workspace::OpenLog.boxed_clone(), cx) - }) - .dismiss_button(true) - }, - ); - workspace.toggle_status_toast(error_toast, cx); - } - }) - .ok(); -} - -#[derive(Default, Copy, Clone)] -pub struct SettingsImportState { - pub cursor: bool, - pub vscode: bool, -} - -impl Global for SettingsImportState {} - -impl SettingsImportState { - pub fn global(cx: &App) -> Self { - cx.try_global().cloned().unwrap_or_default() - } - pub fn update(cx: &mut App, f: impl FnOnce(&mut Self, &mut App) -> R) -> R { - cx.update_default_global(f) - } -} - -impl workspace::SerializableItem for Onboarding { - fn serialized_item_kind() -> &'static str { - "OnboardingPage" - } - - fn cleanup( - workspace_id: workspace::WorkspaceId, - alive_items: Vec, - _window: &mut Window, - cx: &mut App, - ) -> gpui::Task> { - workspace::delete_unloaded_items( - alive_items, - workspace_id, - "onboarding_pages", - &persistence::ONBOARDING_PAGES, - cx, - ) - } - - fn deserialize( - _project: Entity, - workspace: WeakEntity, - workspace_id: workspace::WorkspaceId, - item_id: workspace::ItemId, - window: &mut Window, - cx: &mut App, - ) -> gpui::Task>> { - window.spawn(cx, async move |cx| { - if let Some(page_number) = - persistence::ONBOARDING_PAGES.get_onboarding_page(item_id, workspace_id)? - { - let page = match page_number { - 0 => Some(SelectedPage::Basics), - 1 => Some(SelectedPage::Editing), - 2 => Some(SelectedPage::AiSetup), - _ => None, - }; - workspace.update(cx, |workspace, cx| { - let onboarding_page = Onboarding::new(workspace, cx); - if let Some(page) = page { - zlog::info!("Onboarding page {page:?} loaded"); - onboarding_page.update(cx, |onboarding_page, cx| { - onboarding_page.set_page(page, None, cx); - }) - } - onboarding_page - }) - } else { - Err(anyhow::anyhow!("No onboarding page to deserialize")) - } - }) - } - - fn serialize( - &mut self, - workspace: &mut Workspace, - item_id: workspace::ItemId, - _closing: bool, - _window: &mut Window, - cx: &mut ui::Context, - ) -> Option>> { - let workspace_id = workspace.database_id()?; - let page_number = self.selected_page as u16; - Some(cx.background_spawn(async move { - persistence::ONBOARDING_PAGES - .save_onboarding_page(item_id, workspace_id, page_number) - .await - })) - } - - fn should_serialize(&self, event: &Self::Event) -> bool { - event == &ItemEvent::UpdateTab - } -} - -mod persistence { - use db::{ - query, - sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection}, - sqlez_macros::sql, - }; - use workspace::WorkspaceDb; - - pub struct OnboardingPagesDb(ThreadSafeConnection); - - impl Domain for OnboardingPagesDb { - const NAME: &str = stringify!(OnboardingPagesDb); - - const MIGRATIONS: &[&str] = &[sql!( - CREATE TABLE onboarding_pages ( - workspace_id INTEGER, - item_id INTEGER UNIQUE, - page_number INTEGER, - - PRIMARY KEY(workspace_id, item_id), - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ) STRICT; - )]; - } - - db::static_connection!(ONBOARDING_PAGES, OnboardingPagesDb, [WorkspaceDb]); - - impl OnboardingPagesDb { - query! { - pub async fn save_onboarding_page( - item_id: workspace::ItemId, - workspace_id: workspace::WorkspaceId, - page_number: u16 - ) -> Result<()> { - INSERT OR REPLACE INTO onboarding_pages(item_id, workspace_id, page_number) - VALUES (?, ?, ?) - } - } - - query! { - pub fn get_onboarding_page( - item_id: workspace::ItemId, - workspace_id: workspace::WorkspaceId - ) -> Result> { - SELECT page_number - FROM onboarding_pages - WHERE item_id = ? AND workspace_id = ? - } - } - } + }) + .ok(); } diff --git a/crates/onboarding/src/theme_preview.rs b/crates/onboarding/src/theme_preview.rs index 8bd65d8a27..73b540bd40 100644 --- a/crates/onboarding/src/theme_preview.rs +++ b/crates/onboarding/src/theme_preview.rs @@ -1,320 +1,194 @@ #![allow(unused, dead_code)] use gpui::{Hsla, Length}; -use std::{ - cell::LazyCell, - sync::{Arc, LazyLock, OnceLock}, -}; -use theme::{Theme, ThemeColors, ThemeRegistry}; +use std::sync::Arc; +use theme::{Theme, ThemeRegistry}; use ui::{ IntoElement, RenderOnce, component_prelude::Documented, prelude::*, utils::inner_corner_radius, }; -#[derive(Clone, PartialEq)] -pub enum ThemePreviewStyle { - Bordered, - Borderless, - SideBySide(Arc), -} - /// Shows a preview of a theme as an abstract illustration /// of a thumbnail-sized editor. #[derive(IntoElement, RegisterComponent, Documented)] pub struct ThemePreviewTile { theme: Arc, seed: f32, - style: ThemePreviewStyle, } -static CHILD_RADIUS: LazyLock = LazyLock::new(|| { - inner_corner_radius( - ThemePreviewTile::ROOT_RADIUS, - ThemePreviewTile::ROOT_BORDER, - ThemePreviewTile::ROOT_PADDING, - ThemePreviewTile::CHILD_BORDER, - ) -}); - impl ThemePreviewTile { - pub const SKELETON_HEIGHT_DEFAULT: Pixels = px(2.); - pub const SIDEBAR_SKELETON_ITEM_COUNT: usize = 8; - pub const SIDEBAR_WIDTH_DEFAULT: DefiniteLength = relative(0.25); - pub const ROOT_RADIUS: Pixels = px(8.0); - pub const ROOT_BORDER: Pixels = px(2.0); - pub const ROOT_PADDING: Pixels = px(2.0); - pub const CHILD_BORDER: Pixels = px(1.0); + pub const CORNER_RADIUS: Pixels = px(8.0); pub fn new(theme: Arc, seed: f32) -> Self { - Self { - theme, - seed, - style: ThemePreviewStyle::Bordered, - } - } - - pub fn style(mut self, style: ThemePreviewStyle) -> Self { - self.style = style; - self - } - - pub fn item_skeleton(w: Length, h: Length, bg: Hsla) -> impl IntoElement { - div().w(w).h(h).rounded_full().bg(bg) - } - - pub fn render_sidebar_skeleton_items( - seed: f32, - colors: &ThemeColors, - skeleton_height: impl Into + Clone, - ) -> [impl IntoElement; Self::SIDEBAR_SKELETON_ITEM_COUNT] { - let skeleton_height = skeleton_height.into(); - std::array::from_fn(|index| { - let width = { - let value = (seed * 1000.0 + index as f32 * 10.0).sin() * 0.5 + 0.5; - 0.5 + value * 0.45 - }; - Self::item_skeleton( - relative(width).into(), - skeleton_height, - colors.text.alpha(0.45), - ) - }) - } - - pub fn render_pseudo_code_skeleton( - seed: f32, - theme: Arc, - skeleton_height: impl Into, - ) -> impl IntoElement { - let colors = theme.colors(); - let syntax = theme.syntax(); - - let keyword_color = syntax.get("keyword").color; - let function_color = syntax.get("function").color; - let string_color = syntax.get("string").color; - let comment_color = syntax.get("comment").color; - let variable_color = syntax.get("variable").color; - let type_color = syntax.get("type").color; - let punctuation_color = syntax.get("punctuation").color; - - let syntax_colors = [ - keyword_color, - function_color, - string_color, - variable_color, - type_color, - punctuation_color, - comment_color, - ]; - - let skeleton_height = skeleton_height.into(); - - let line_width = |line_idx: usize, block_idx: usize| -> f32 { - let val = - (seed * 100.0 + line_idx as f32 * 20.0 + block_idx as f32 * 5.0).sin() * 0.5 + 0.5; - 0.05 + val * 0.2 - }; - - let indentation = |line_idx: usize| -> f32 { - let step = line_idx % 6; - if step < 3 { - step as f32 * 0.1 - } else { - (5 - step) as f32 * 0.1 - } - }; - - let pick_color = |line_idx: usize, block_idx: usize| -> Hsla { - let idx = ((seed * 10.0 + line_idx as f32 * 7.0 + block_idx as f32 * 3.0).sin() * 3.5) - .abs() as usize - % syntax_colors.len(); - syntax_colors[idx].unwrap_or(colors.text) - }; - - let line_count = 13; - - let lines = (0..line_count) - .map(|line_idx| { - let block_count = (((seed * 30.0 + line_idx as f32 * 12.0).sin() * 0.5 + 0.5) * 3.0) - .round() as usize - + 2; - - let indent = indentation(line_idx); - - let blocks = (0..block_count) - .map(|block_idx| { - let width = line_width(line_idx, block_idx); - let color = pick_color(line_idx, block_idx); - Self::item_skeleton(relative(width).into(), skeleton_height, color) - }) - .collect::>(); - - h_flex().gap(px(2.)).ml(relative(indent)).children(blocks) - }) - .collect::>(); - - v_flex().size_full().p_1().gap_1p5().children(lines) - } - - pub fn render_sidebar( - seed: f32, - colors: &ThemeColors, - width: impl Into + Clone, - skeleton_height: impl Into, - ) -> impl IntoElement { - div() - .h_full() - .w(width) - .border_r(px(1.)) - .border_color(colors.border_transparent) - .bg(colors.panel_background) - .child(v_flex().p_2().size_full().gap_1().children( - Self::render_sidebar_skeleton_items(seed, colors, skeleton_height.into()), - )) - } - - pub fn render_pane( - seed: f32, - theme: Arc, - skeleton_height: impl Into, - ) -> impl IntoElement { - v_flex().h_full().flex_grow().child( - div() - .size_full() - .overflow_hidden() - .bg(theme.colors().editor_background) - .p_2() - .child(Self::render_pseudo_code_skeleton( - seed, - theme, - skeleton_height.into(), - )), - ) - } - - pub fn render_editor( - seed: f32, - theme: Arc, - sidebar_width: impl Into + Clone, - skeleton_height: impl Into + Clone, - ) -> impl IntoElement { - div() - .size_full() - .flex() - .bg(theme.colors().background.alpha(1.00)) - .child(Self::render_sidebar( - seed, - theme.colors(), - sidebar_width, - skeleton_height.clone(), - )) - .child(Self::render_pane(seed, theme, skeleton_height)) - } - - fn render_borderless(seed: f32, theme: Arc) -> impl IntoElement { - Self::render_editor( - seed, - theme, - Self::SIDEBAR_WIDTH_DEFAULT, - Self::SKELETON_HEIGHT_DEFAULT, - ) - } - - fn render_border(seed: f32, theme: Arc) -> impl IntoElement { - div() - .size_full() - .p(Self::ROOT_PADDING) - .rounded(Self::ROOT_RADIUS) - .child( - div() - .size_full() - .rounded(*CHILD_RADIUS) - .border(Self::CHILD_BORDER) - .border_color(theme.colors().border) - .child(Self::render_editor( - seed, - theme.clone(), - Self::SIDEBAR_WIDTH_DEFAULT, - Self::SKELETON_HEIGHT_DEFAULT, - )), - ) - } - - fn render_side_by_side( - seed: f32, - theme: Arc, - other_theme: Arc, - border_color: Hsla, - ) -> impl IntoElement { - let sidebar_width = relative(0.20); - - div() - .size_full() - .p(Self::ROOT_PADDING) - .rounded(Self::ROOT_RADIUS) - .child( - h_flex() - .size_full() - .relative() - .rounded(*CHILD_RADIUS) - .border(Self::CHILD_BORDER) - .border_color(border_color) - .overflow_hidden() - .child(div().size_full().child(Self::render_editor( - seed, - theme, - sidebar_width, - Self::SKELETON_HEIGHT_DEFAULT, - ))) - .child( - div() - .size_full() - .absolute() - .left_1_2() - .bg(other_theme.colors().editor_background) - .child(Self::render_editor( - seed, - other_theme, - sidebar_width, - Self::SKELETON_HEIGHT_DEFAULT, - )), - ), - ) - .into_any_element() + Self { theme, seed } } } impl RenderOnce for ThemePreviewTile { fn render(self, _window: &mut ui::Window, _cx: &mut ui::App) -> impl IntoElement { - match self.style { - ThemePreviewStyle::Bordered => { - Self::render_border(self.seed, self.theme).into_any_element() - } - ThemePreviewStyle::Borderless => { - Self::render_borderless(self.seed, self.theme).into_any_element() - } - ThemePreviewStyle::SideBySide(other_theme) => Self::render_side_by_side( - self.seed, - self.theme, - other_theme, - _cx.theme().colors().border, + let color = self.theme.colors(); + + let root_radius = Self::CORNER_RADIUS; + let root_border = px(2.0); + let root_padding = px(2.0); + let child_border = px(1.0); + let inner_radius = + inner_corner_radius(root_radius, root_border, root_padding, child_border); + + let item_skeleton = |w: Length, h: Pixels, bg: Hsla| div().w(w).h(h).rounded_full().bg(bg); + + let skeleton_height = px(4.); + + let sidebar_seeded_width = |seed: f32, index: usize| { + let value = (seed * 1000.0 + index as f32 * 10.0).sin() * 0.5 + 0.5; + 0.5 + value * 0.45 + }; + + let sidebar_skeleton_items = 8; + + let sidebar_skeleton = (0..sidebar_skeleton_items) + .map(|i| { + let width = sidebar_seeded_width(self.seed, i); + item_skeleton( + relative(width).into(), + skeleton_height, + color.text.alpha(0.45), + ) + }) + .collect::>(); + + let sidebar = div() + .h_full() + .w(relative(0.25)) + .border_r(px(1.)) + .border_color(color.border_transparent) + .bg(color.panel_background) + .child( + div() + .p_2() + .flex() + .flex_col() + .size_full() + .gap(px(4.)) + .children(sidebar_skeleton), + ); + + let pseudo_code_skeleton = |theme: Arc, seed: f32| -> AnyElement { + let colors = theme.colors(); + let syntax = theme.syntax(); + + let keyword_color = syntax.get("keyword").color; + let function_color = syntax.get("function").color; + let string_color = syntax.get("string").color; + let comment_color = syntax.get("comment").color; + let variable_color = syntax.get("variable").color; + let type_color = syntax.get("type").color; + let punctuation_color = syntax.get("punctuation").color; + + let syntax_colors = [ + keyword_color, + function_color, + string_color, + variable_color, + type_color, + punctuation_color, + comment_color, + ]; + + let line_width = |line_idx: usize, block_idx: usize| -> f32 { + let val = (seed * 100.0 + line_idx as f32 * 20.0 + block_idx as f32 * 5.0).sin() + * 0.5 + + 0.5; + 0.05 + val * 0.2 + }; + + let indentation = |line_idx: usize| -> f32 { + let step = line_idx % 6; + if step < 3 { + step as f32 * 0.1 + } else { + (5 - step) as f32 * 0.1 + } + }; + + let pick_color = |line_idx: usize, block_idx: usize| -> Hsla { + let idx = ((seed * 10.0 + line_idx as f32 * 7.0 + block_idx as f32 * 3.0).sin() + * 3.5) + .abs() as usize + % syntax_colors.len(); + syntax_colors[idx].unwrap_or(colors.text) + }; + + let line_count = 13; + + let lines = (0..line_count) + .map(|line_idx| { + let block_count = (((seed * 30.0 + line_idx as f32 * 12.0).sin() * 0.5 + 0.5) + * 3.0) + .round() as usize + + 2; + + let indent = indentation(line_idx); + + let blocks = (0..block_count) + .map(|block_idx| { + let width = line_width(line_idx, block_idx); + let color = pick_color(line_idx, block_idx); + item_skeleton(relative(width).into(), skeleton_height, color) + }) + .collect::>(); + + h_flex().gap(px(2.)).ml(relative(indent)).children(blocks) + }) + .collect::>(); + + v_flex() + .size_full() + .p_1() + .gap(px(6.)) + .children(lines) + .into_any_element() + }; + + let pane = div() + .h_full() + .flex_grow() + .flex() + .flex_col() + // .child( + // div() + // .w_full() + // .border_color(color.border) + // .border_b(px(1.)) + // .h(relative(0.1)) + // .bg(color.tab_bar_background), + // ) + .child( + div() + .size_full() + .overflow_hidden() + .bg(color.editor_background) + .p_2() + .child(pseudo_code_skeleton(self.theme.clone(), self.seed)), + ); + + let content = div().size_full().flex().child(sidebar).child(pane); + + div() + .size_full() + .rounded(root_radius) + .p(root_padding) + .child( + div() + .size_full() + .rounded(inner_radius) + .border(child_border) + .border_color(color.border) + .bg(color.background) + .child(content), ) - .into_any_element(), - } } } impl Component for ThemePreviewTile { - fn scope() -> ComponentScope { - ComponentScope::Onboarding - } - - fn name() -> &'static str { - "Theme Preview Tile" - } - - fn sort_name() -> &'static str { - "Theme Preview Tile" - } - fn description() -> Option<&'static str> { Some(Self::DOCS) } @@ -329,9 +203,9 @@ impl Component for ThemePreviewTile { let themes_to_preview = vec![ one_dark.clone().ok(), - one_light.ok(), - gruvbox_dark.ok(), - gruvbox_light.ok(), + one_light.clone().ok(), + gruvbox_dark.clone().ok(), + gruvbox_light.clone().ok(), ] .into_iter() .flatten() @@ -348,7 +222,7 @@ impl Component for ThemePreviewTile { div() .w(px(240.)) .h(px(180.)) - .child(ThemePreviewTile::new(one_dark, 0.42)) + .child(ThemePreviewTile::new(one_dark.clone(), 0.42)) .into_any_element(), )])] } else { @@ -362,12 +236,13 @@ impl Component for ThemePreviewTile { .gap_4() .children( themes_to_preview - .into_iter() - .map(|theme| { + .iter() + .enumerate() + .map(|(_, theme)| { div() .w(px(200.)) .h(px(140.)) - .child(ThemePreviewTile::new(theme, 0.42)) + .child(ThemePreviewTile::new(theme.clone(), 0.42)) }) .collect::>(), ) diff --git a/crates/onboarding/src/welcome.rs b/crates/onboarding/src/welcome.rs index 8ff55d812b..9e524a5e8a 100644 --- a/crates/onboarding/src/welcome.rs +++ b/crates/onboarding/src/welcome.rs @@ -1,18 +1,14 @@ use gpui::{ Action, App, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, - ParentElement, Render, Styled, Task, Window, actions, + NoAction, ParentElement, Render, Styled, Window, actions, }; -use menu::{SelectNext, SelectPrevious}; use ui::{ButtonLike, Divider, DividerColor, KeyBinding, Vector, VectorName, prelude::*}; use workspace::{ - NewFile, Open, WorkspaceId, + NewFile, Open, Workspace, WorkspaceId, item::{Item, ItemEvent}, - with_active_or_new_workspace, }; use zed_actions::{Extensions, OpenSettings, agent, command_palette}; -use crate::{Onboarding, OpenOnboarding}; - actions!( zed, [ @@ -37,8 +33,9 @@ const CONTENT: (Section<4>, Section<3>) = ( }, SectionEntry { icon: IconName::CloudDownload, - title: "Clone Repository", - action: &git::Clone, + title: "Clone a Repo", + // TODO: use proper action + action: &NoAction, }, SectionEntry { icon: IconName::ListCollapse, @@ -87,24 +84,24 @@ impl Section { ) -> impl IntoElement { v_flex() .min_w_full() + .gap_2() .child( h_flex() .px_1() - .mb_2() - .gap_2() + .gap_4() .child( Label::new(self.title.to_ascii_uppercase()) .buffer_font(cx) .color(Color::Muted) .size(LabelSize::XSmall), ) - .child(Divider::horizontal().color(DividerColor::BorderVariant)), + .child(Divider::horizontal().color(DividerColor::Border)), ) .children( self.entries .iter() .enumerate() - .map(|(index, entry)| entry.render(index_offset + index, focus, window, cx)), + .map(|(index, entry)| entry.render(index_offset + index, &focus, window, cx)), ) } } @@ -124,12 +121,11 @@ impl SectionEntry { cx: &App, ) -> impl IntoElement { ButtonLike::new(("onboarding-button-id", button_index)) - .tab_index(button_index as isize) .full_width() - .size(ButtonSize::Medium) .child( h_flex() .w_full() + .gap_1() .justify_between() .child( h_flex() @@ -141,10 +137,7 @@ impl SectionEntry { ) .child(Label::new(self.title)), ) - .children( - KeyBinding::for_action_in(self.action, focus, window, cx) - .map(|s| s.size(rems_from_px(12.))), - ), + .children(KeyBinding::for_action_in(self.action, focus, window, cx)), ) .on_click(|_, window, cx| window.dispatch_action(self.action.boxed_clone(), cx)) } @@ -154,23 +147,10 @@ pub struct WelcomePage { focus_handle: FocusHandle, } -impl WelcomePage { - fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context) { - window.focus_next(); - cx.notify(); - } - - fn select_previous(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context) { - window.focus_prev(); - cx.notify(); - } -} - impl Render for WelcomePage { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let (first_section, second_section) = CONTENT; + let (first_section, second_entries) = CONTENT; let first_section_entries = first_section.entries.len(); - let last_index = first_section_entries + second_section.entries.len(); h_flex() .size_full() @@ -179,8 +159,6 @@ impl Render for WelcomePage { .bg(cx.theme().colors().editor_background) .key_context("Welcome") .track_focus(&self.focus_handle(cx)) - .on_action(cx.listener(Self::select_previous)) - .on_action(cx.listener(Self::select_next)) .child( h_flex() .px_12() @@ -210,15 +188,15 @@ impl Render for WelcomePage { ) .child( v_flex() - .mt_10() - .gap_6() + .mt_12() + .gap_8() .child(first_section.render( Default::default(), &self.focus_handle, window, cx, )) - .child(second_section.render( + .child(second_entries.render( first_section_entries, &self.focus_handle, window, @@ -232,71 +210,15 @@ impl Render for WelcomePage { // We call this a hack .rounded_b_xs() .border_t_1() - .border_color(cx.theme().colors().border.opacity(0.6)) + .border_color(DividerColor::Border.hsla(cx)) .border_dashed() .child( + div().child( Button::new("welcome-exit", "Return to Setup") - .tab_index(last_index as isize) .full_width() - .label_size(LabelSize::XSmall) - .on_click(|_, window, cx| { - window.dispatch_action( - OpenOnboarding.boxed_clone(), - cx, - ); - - with_active_or_new_workspace(cx, |workspace, window, cx| { - let Some((welcome_id, welcome_idx)) = workspace - .active_pane() - .read(cx) - .items() - .enumerate() - .find_map(|(idx, item)| { - let _ = item.downcast::()?; - Some((item.item_id(), idx)) - }) - else { - return; - }; - - workspace.active_pane().update(cx, |pane, cx| { - // Get the index here to get around the borrow checker - let idx = pane.items().enumerate().find_map( - |(idx, item)| { - let _ = - item.downcast::()?; - Some(idx) - }, - ); - - if let Some(idx) = idx { - pane.activate_item( - idx, true, true, window, cx, - ); - } else { - let item = - Box::new(Onboarding::new(workspace, cx)); - pane.add_item( - item, - true, - true, - Some(welcome_idx), - window, - cx, - ); - } - - pane.remove_item( - welcome_id, - false, - false, - window, - cx, - ); - }); - }); - }), + .label_size(LabelSize::XSmall), ), + ), ), ), ), @@ -305,7 +227,7 @@ impl Render for WelcomePage { } impl WelcomePage { - pub fn new(window: &mut Window, cx: &mut App) -> Entity { + pub fn new(window: &mut Window, cx: &mut Context) -> Entity { cx.new(|cx| { let focus_handle = cx.focus_handle(); cx.on_focus(&focus_handle, window, |_, _, cx| cx.notify()) @@ -352,116 +274,3 @@ impl Item for WelcomePage { f(*event) } } - -impl workspace::SerializableItem for WelcomePage { - fn serialized_item_kind() -> &'static str { - "WelcomePage" - } - - fn cleanup( - workspace_id: workspace::WorkspaceId, - alive_items: Vec, - _window: &mut Window, - cx: &mut App, - ) -> Task> { - workspace::delete_unloaded_items( - alive_items, - workspace_id, - "welcome_pages", - &persistence::WELCOME_PAGES, - cx, - ) - } - - fn deserialize( - _project: Entity, - _workspace: gpui::WeakEntity, - workspace_id: workspace::WorkspaceId, - item_id: workspace::ItemId, - window: &mut Window, - cx: &mut App, - ) -> Task>> { - if persistence::WELCOME_PAGES - .get_welcome_page(item_id, workspace_id) - .ok() - .is_some_and(|is_open| is_open) - { - window.spawn(cx, async move |cx| cx.update(WelcomePage::new)) - } else { - Task::ready(Err(anyhow::anyhow!("No welcome page to deserialize"))) - } - } - - fn serialize( - &mut self, - workspace: &mut workspace::Workspace, - item_id: workspace::ItemId, - _closing: bool, - _window: &mut Window, - cx: &mut Context, - ) -> Option>> { - let workspace_id = workspace.database_id()?; - Some(cx.background_spawn(async move { - persistence::WELCOME_PAGES - .save_welcome_page(item_id, workspace_id, true) - .await - })) - } - - fn should_serialize(&self, event: &Self::Event) -> bool { - event == &ItemEvent::UpdateTab - } -} - -mod persistence { - use db::{ - query, - sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection}, - sqlez_macros::sql, - }; - use workspace::WorkspaceDb; - - pub struct WelcomePagesDb(ThreadSafeConnection); - - impl Domain for WelcomePagesDb { - const NAME: &str = stringify!(WelcomePagesDb); - - const MIGRATIONS: &[&str] = (&[sql!( - CREATE TABLE welcome_pages ( - workspace_id INTEGER, - item_id INTEGER UNIQUE, - is_open INTEGER DEFAULT FALSE, - - PRIMARY KEY(workspace_id, item_id), - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ) STRICT; - )]); - } - - db::static_connection!(WELCOME_PAGES, WelcomePagesDb, [WorkspaceDb]); - - impl WelcomePagesDb { - query! { - pub async fn save_welcome_page( - item_id: workspace::ItemId, - workspace_id: workspace::WorkspaceId, - is_open: bool - ) -> Result<()> { - INSERT OR REPLACE INTO welcome_pages(item_id, workspace_id, is_open) - VALUES (?, ?, ?) - } - } - - query! { - pub fn get_welcome_page( - item_id: workspace::ItemId, - workspace_id: workspace::WorkspaceId - ) -> Result { - SELECT is_open - FROM welcome_pages - WHERE item_id = ? AND workspace_id = ? - } - } - } -} diff --git a/crates/open_ai/Cargo.toml b/crates/open_ai/Cargo.toml index bae00f0a8e..2d40cd2735 100644 --- a/crates/open_ai/Cargo.toml +++ b/crates/open_ai/Cargo.toml @@ -20,7 +20,6 @@ anyhow.workspace = true futures.workspace = true http_client.workspace = true schemars = { workspace = true, optional = true } -log.workspace = true serde.workspace = true serde_json.workspace = true strum.workspace = true diff --git a/crates/open_ai/src/open_ai.rs b/crates/open_ai/src/open_ai.rs index 08be82b830..12a5cf52d2 100644 --- a/crates/open_ai/src/open_ai.rs +++ b/crates/open_ai/src/open_ai.rs @@ -9,7 +9,7 @@ use strum::EnumIter; pub const OPEN_AI_API_URL: &str = "https://api.openai.com/v1"; fn is_none_or_empty, U>(opt: &Option) -> bool { - opt.as_ref().is_none_or(|v| v.as_ref().is_empty()) + opt.as_ref().map_or(true, |v| v.as_ref().is_empty()) } #[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)] @@ -74,12 +74,6 @@ pub enum Model { O3, #[serde(rename = "o4-mini")] O4Mini, - #[serde(rename = "gpt-5")] - Five, - #[serde(rename = "gpt-5-mini")] - FiveMini, - #[serde(rename = "gpt-5-nano")] - FiveNano, #[serde(rename = "custom")] Custom { @@ -89,13 +83,11 @@ pub enum Model { max_tokens: u64, max_output_tokens: Option, max_completion_tokens: Option, - reasoning_effort: Option, }, } impl Model { pub fn default_fast() -> Self { - // TODO: Replace with FiveMini since all other models are deprecated Self::FourPointOneMini } @@ -113,9 +105,6 @@ impl Model { "o3-mini" => Ok(Self::O3Mini), "o3" => Ok(Self::O3), "o4-mini" => Ok(Self::O4Mini), - "gpt-5" => Ok(Self::Five), - "gpt-5-mini" => Ok(Self::FiveMini), - "gpt-5-nano" => Ok(Self::FiveNano), invalid_id => anyhow::bail!("invalid model id '{invalid_id}'"), } } @@ -134,9 +123,6 @@ impl Model { Self::O3Mini => "o3-mini", Self::O3 => "o3", Self::O4Mini => "o4-mini", - Self::Five => "gpt-5", - Self::FiveMini => "gpt-5-mini", - Self::FiveNano => "gpt-5-nano", Self::Custom { name, .. } => name, } } @@ -155,9 +141,6 @@ impl Model { Self::O3Mini => "o3-mini", Self::O3 => "o3", Self::O4Mini => "o4-mini", - Self::Five => "gpt-5", - Self::FiveMini => "gpt-5-mini", - Self::FiveNano => "gpt-5-nano", Self::Custom { name, display_name, .. } => display_name.as_ref().unwrap_or(name), @@ -178,9 +161,6 @@ impl Model { Self::O3Mini => 200_000, Self::O3 => 200_000, Self::O4Mini => 200_000, - Self::Five => 272_000, - Self::FiveMini => 272_000, - Self::FiveNano => 272_000, Self::Custom { max_tokens, .. } => *max_tokens, } } @@ -202,18 +182,6 @@ impl Model { Self::O3Mini => Some(100_000), Self::O3 => Some(100_000), Self::O4Mini => Some(100_000), - Self::Five => Some(128_000), - Self::FiveMini => Some(128_000), - Self::FiveNano => Some(128_000), - } - } - - pub fn reasoning_effort(&self) -> Option { - match self { - Self::Custom { - reasoning_effort, .. - } => reasoning_effort.to_owned(), - _ => None, } } @@ -229,20 +197,10 @@ impl Model { | Self::FourOmniMini | Self::FourPointOne | Self::FourPointOneMini - | Self::FourPointOneNano - | Self::Five - | Self::FiveMini - | Self::FiveNano => true, + | Self::FourPointOneNano => true, Self::O1 | Self::O3 | Self::O3Mini | Self::O4Mini | Model::Custom { .. } => false, } } - - /// Returns whether the given model supports the `prompt_cache_key` parameter. - /// - /// If the model does not support the parameter, do not pass it up. - pub fn supports_prompt_cache_key(&self) -> bool { - true - } } #[derive(Debug, Serialize, Deserialize)] @@ -262,10 +220,6 @@ pub struct Request { pub parallel_tool_calls: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub tools: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub prompt_cache_key: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub reasoning_effort: Option, } #[derive(Debug, Serialize, Deserialize)] @@ -277,16 +231,6 @@ pub enum ToolChoice { Other(ToolDefinition), } -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] -#[serde(rename_all = "lowercase")] -pub enum ReasoningEffort { - Minimal, - Low, - Medium, - High, -} - #[derive(Clone, Deserialize, Serialize, Debug)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ToolDefinition { @@ -432,20 +376,16 @@ pub struct ChoiceDelta { pub finish_reason: Option, } -#[derive(Serialize, Deserialize, Debug)] -pub struct OpenAiError { - message: String, -} - #[derive(Serialize, Deserialize, Debug)] #[serde(untagged)] pub enum ResponseStreamResult { Ok(ResponseStreamEvent), - Err { error: OpenAiError }, + Err { error: String }, } #[derive(Serialize, Deserialize, Debug)] pub struct ResponseStreamEvent { + pub model: String, pub choices: Vec, pub usage: Option, } @@ -479,17 +419,9 @@ pub async fn stream_completion( match serde_json::from_str(line) { Ok(ResponseStreamResult::Ok(response)) => Some(Ok(response)), Ok(ResponseStreamResult::Err { error }) => { - Some(Err(anyhow!(error.message))) - } - Err(error) => { - log::error!( - "Failed to parse OpenAI response into ResponseStreamResult: `{}`\n\ - Response: `{}`", - error, - line, - ); Some(Err(anyhow!(error))) } + Err(error) => Some(Err(anyhow!(error))), } } } @@ -506,6 +438,11 @@ pub async fn stream_completion( error: OpenAiError, } + #[derive(Deserialize)] + struct OpenAiError { + message: String, + } + match serde_json::from_str::(&body) { Ok(response) if !response.error.message.is_empty() => Err(anyhow!( "API request to {} failed: {}", diff --git a/crates/open_router/src/open_router.rs b/crates/open_router/src/open_router.rs index b7e6d69d8f..3e6e406d98 100644 --- a/crates/open_router/src/open_router.rs +++ b/crates/open_router/src/open_router.rs @@ -8,7 +8,7 @@ use std::convert::TryFrom; pub const OPEN_ROUTER_API_URL: &str = "https://openrouter.ai/api/v1"; fn is_none_or_empty, U>(opt: &Option) -> bool { - opt.as_ref().is_none_or(|v| v.as_ref().is_empty()) + opt.as_ref().map_or(true, |v| v.as_ref().is_empty()) } #[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)] @@ -240,10 +240,10 @@ impl MessageContent { impl From> for MessageContent { fn from(parts: Vec) -> Self { - if parts.len() == 1 - && let MessagePart::Text { text } = &parts[0] - { - return Self::Plain(text.clone()); + if parts.len() == 1 { + if let MessagePart::Text { text } = &parts[0] { + return Self::Plain(text.clone()); + } } Self::Multipart(parts) } diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 10698cead8..50c6c2dcce 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -503,16 +503,16 @@ impl SearchData { && multi_buffer_snapshot .chars_at(extended_context_left_border) .last() - .is_some_and(|c| !c.is_whitespace()); + .map_or(false, |c| !c.is_whitespace()); let truncated_right = entire_context_text .chars() .last() - .is_none_or(|c| !c.is_whitespace()) + .map_or(true, |c| !c.is_whitespace()) && extended_context_right_border > context_right_border && multi_buffer_snapshot .chars_at(extended_context_right_border) .next() - .is_some_and(|c| !c.is_whitespace()); + .map_or(false, |c| !c.is_whitespace()); search_match_indices.iter_mut().for_each(|range| { range.start = multi_buffer_snapshot.clip_offset( range.start.saturating_sub(left_whitespaces_offset), @@ -733,8 +733,7 @@ impl OutlinePanel { ) -> Entity { let project = workspace.project().clone(); let workspace_handle = cx.entity().downgrade(); - - cx.new(|cx| { + let outline_panel = cx.new(|cx| { let filter_editor = cx.new(|cx| { let mut editor = Editor::single_line(window, cx); editor.set_placeholder_text("Filter...", cx); @@ -913,7 +912,9 @@ impl OutlinePanel { outline_panel.replace_active_editor(item, editor, window, cx); } outline_panel - }) + }); + + outline_panel } fn serialization_key(workspace: &Workspace) -> Option { @@ -1040,7 +1041,7 @@ impl OutlinePanel { fn open_excerpts( &mut self, - action: &editor::actions::OpenExcerpts, + action: &editor::OpenExcerpts, window: &mut Window, cx: &mut Context, ) { @@ -1056,7 +1057,7 @@ impl OutlinePanel { fn open_excerpts_split( &mut self, - action: &editor::actions::OpenExcerptsSplit, + action: &editor::OpenExcerptsSplit, window: &mut Window, cx: &mut Context, ) { @@ -1169,11 +1170,12 @@ impl OutlinePanel { }); } else { let mut offset = Point::default(); - if let Some(buffer_id) = scroll_to_buffer - && multi_buffer_snapshot.as_singleton().is_none() - && !active_editor.read(cx).is_buffer_folded(buffer_id, cx) - { - offset.y = -(active_editor.read(cx).file_header_size() as f32); + if let Some(buffer_id) = scroll_to_buffer { + if multi_buffer_snapshot.as_singleton().is_none() + && !active_editor.read(cx).is_buffer_folded(buffer_id, cx) + { + offset.y = -(active_editor.read(cx).file_header_size() as f32); + } } active_editor.update(cx, |editor, cx| { @@ -1258,7 +1260,7 @@ impl OutlinePanel { dirs_worktree_id == worktree_id && dirs .last() - .is_some_and(|dir| dir.path.as_ref() == parent_path) + .map_or(false, |dir| dir.path.as_ref() == parent_path) } _ => false, }) @@ -1452,7 +1454,9 @@ impl OutlinePanel { if self .unfolded_dirs .get(&directory_worktree) - .is_none_or(|unfolded_dirs| !unfolded_dirs.contains(&directory_entry.id)) + .map_or(true, |unfolded_dirs| { + !unfolded_dirs.contains(&directory_entry.id) + }) { return false; } @@ -1602,14 +1606,16 @@ impl OutlinePanel { } PanelEntry::FoldedDirs(folded_dirs) => { let mut folded = false; - if let Some(dir_entry) = folded_dirs.entries.last() - && self + if let Some(dir_entry) = folded_dirs.entries.last() { + if self .collapsed_entries .insert(CollapsedEntry::Dir(folded_dirs.worktree_id, dir_entry.id)) - { - folded = true; - buffers_to_fold - .extend(self.buffers_inside_directory(folded_dirs.worktree_id, dir_entry)); + { + folded = true; + buffers_to_fold.extend( + self.buffers_inside_directory(folded_dirs.worktree_id, dir_entry), + ); + } } folded } @@ -2102,11 +2108,11 @@ impl OutlinePanel { dirs_to_expand.push(current_entry.id); } - if traversal.back_to_parent() - && let Some(parent_entry) = traversal.entry() - { - current_entry = parent_entry.clone(); - continue; + if traversal.back_to_parent() { + if let Some(parent_entry) = traversal.entry() { + current_entry = parent_entry.clone(); + continue; + } } break; } @@ -2153,7 +2159,7 @@ impl OutlinePanel { ExcerptOutlines::Invalidated(outlines) => Some(outlines), ExcerptOutlines::NotFetched => None, }) - .is_some_and(|outlines| !outlines.is_empty()); + .map_or(false, |outlines| !outlines.is_empty()); let is_expanded = !self .collapsed_entries .contains(&CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id)); @@ -2469,17 +2475,17 @@ impl OutlinePanel { let search_data = match render_data.get() { Some(search_data) => search_data, None => { - if let ItemsDisplayMode::Search(search_state) = &mut self.mode - && let Some(multi_buffer_snapshot) = multi_buffer_snapshot - { - search_state - .highlight_search_match_tx - .try_send(HighlightArguments { - multi_buffer_snapshot: multi_buffer_snapshot.clone(), - match_range: match_range.clone(), - search_data: Arc::clone(render_data), - }) - .ok(); + if let ItemsDisplayMode::Search(search_state) = &mut self.mode { + if let Some(multi_buffer_snapshot) = multi_buffer_snapshot { + search_state + .highlight_search_match_tx + .try_send(HighlightArguments { + multi_buffer_snapshot: multi_buffer_snapshot.clone(), + match_range: match_range.clone(), + search_data: Arc::clone(render_data), + }) + .ok(); + } } return None; } @@ -2564,11 +2570,11 @@ impl OutlinePanel { .on_click({ let clicked_entry = rendered_entry.clone(); cx.listener(move |outline_panel, event: &gpui::ClickEvent, window, cx| { - if event.is_right_click() || event.first_focus() { + if event.down.button == MouseButton::Right || event.down.first_mouse { return; } - let change_focus = event.click_count() > 1; + let change_focus = event.down.click_count > 1; outline_panel.toggle_expanded(&clicked_entry, window, cx); outline_panel.scroll_editor_to_entry( @@ -2623,7 +2629,7 @@ impl OutlinePanel { } fn entry_name(&self, worktree_id: &WorktreeId, entry: &Entry, cx: &App) -> String { - match self.project.read(cx).worktree_for_id(*worktree_id, cx) { + let name = match self.project.read(cx).worktree_for_id(*worktree_id, cx) { Some(worktree) => { let worktree = worktree.read(cx); match worktree.snapshot().root_entry() { @@ -2644,7 +2650,8 @@ impl OutlinePanel { } } None => file_name(entry.path.as_ref()), - } + }; + name } fn update_fs_entries( @@ -2679,8 +2686,7 @@ impl OutlinePanel { new_collapsed_entries = outline_panel.collapsed_entries.clone(); new_unfolded_dirs = outline_panel.unfolded_dirs.clone(); let multi_buffer_snapshot = active_multi_buffer.read(cx).snapshot(cx); - - multi_buffer_snapshot.excerpts().fold( + let buffer_excerpts = multi_buffer_snapshot.excerpts().fold( HashMap::default(), |mut buffer_excerpts, (excerpt_id, buffer_snapshot, excerpt_range)| { let buffer_id = buffer_snapshot.remote_id(); @@ -2727,7 +2733,8 @@ impl OutlinePanel { ); buffer_excerpts }, - ) + ); + buffer_excerpts }) else { return; }; @@ -2826,12 +2833,11 @@ impl OutlinePanel { let new_entry_added = entries_to_add .insert(current_entry.id, current_entry) .is_none(); - if new_entry_added - && traversal.back_to_parent() - && let Some(parent_entry) = traversal.entry() - { - current_entry = parent_entry.to_owned(); - continue; + if new_entry_added && traversal.back_to_parent() { + if let Some(parent_entry) = traversal.entry() { + current_entry = parent_entry.to_owned(); + continue; + } } break; } @@ -2872,17 +2878,18 @@ impl OutlinePanel { entries .into_iter() .filter_map(|entry| { - if auto_fold_dirs && let Some(parent) = entry.path.parent() - { - let children = new_children_count - .entry(worktree_id) - .or_default() - .entry(Arc::from(parent)) - .or_default(); - if entry.is_dir() { - children.dirs += 1; - } else { - children.files += 1; + if auto_fold_dirs { + if let Some(parent) = entry.path.parent() { + let children = new_children_count + .entry(worktree_id) + .or_default() + .entry(Arc::from(parent)) + .or_default(); + if entry.is_dir() { + children.dirs += 1; + } else { + children.files += 1; + } } } @@ -2949,7 +2956,7 @@ impl OutlinePanel { .map(|(parent_dir_id, _)| { new_unfolded_dirs .get(&directory.worktree_id) - .is_none_or(|unfolded_dirs| { + .map_or(true, |unfolded_dirs| { unfolded_dirs .contains(parent_dir_id) }) @@ -3402,29 +3409,30 @@ impl OutlinePanel { { excerpt.outlines = ExcerptOutlines::Outlines(fetched_outlines); - if let Some(default_depth) = pending_default_depth - && let ExcerptOutlines::Outlines(outlines) = + if let Some(default_depth) = pending_default_depth { + if let ExcerptOutlines::Outlines(outlines) = &excerpt.outlines - { - outlines - .iter() - .filter(|outline| { - (default_depth == 0 - || outline.depth >= default_depth) - && outlines_with_children.contains(&( - outline.range.clone(), - outline.depth, - )) - }) - .for_each(|outline| { - outline_panel.collapsed_entries.insert( - CollapsedEntry::Outline( - buffer_id, - excerpt_id, - outline.range.clone(), - ), - ); - }); + { + outlines + .iter() + .filter(|outline| { + (default_depth == 0 + || outline.depth >= default_depth) + && outlines_with_children.contains(&( + outline.range.clone(), + outline.depth, + )) + }) + .for_each(|outline| { + outline_panel.collapsed_entries.insert( + CollapsedEntry::Outline( + buffer_id, + excerpt_id, + outline.range.clone(), + ), + ); + }); + } } // Even if no outlines to check, we still need to update cached entries @@ -3440,8 +3448,9 @@ impl OutlinePanel { } fn is_singleton_active(&self, cx: &App) -> bool { - self.active_editor() - .is_some_and(|active_editor| active_editor.read(cx).buffer().read(cx).is_singleton()) + self.active_editor().map_or(false, |active_editor| { + active_editor.read(cx).buffer().read(cx).is_singleton() + }) } fn invalidate_outlines(&mut self, ids: &[ExcerptId]) { @@ -3602,9 +3611,10 @@ impl OutlinePanel { .update_in(cx, |outline_panel, window, cx| { outline_panel.cached_entries = new_cached_entries; outline_panel.max_width_item_index = max_width_item_index; - if (outline_panel.selected_entry.is_invalidated() - || matches!(outline_panel.selected_entry, SelectedEntry::None)) - && let Some(new_selected_entry) = + if outline_panel.selected_entry.is_invalidated() + || matches!(outline_panel.selected_entry, SelectedEntry::None) + { + if let Some(new_selected_entry) = outline_panel.active_editor().and_then(|active_editor| { outline_panel.location_for_editor_selection( &active_editor, @@ -3612,8 +3622,9 @@ impl OutlinePanel { cx, ) }) - { - outline_panel.select_entry(new_selected_entry, false, window, cx); + { + outline_panel.select_entry(new_selected_entry, false, window, cx); + } } outline_panel.autoscroll(cx); @@ -3659,7 +3670,7 @@ impl OutlinePanel { let is_root = project .read(cx) .worktree_for_id(directory_entry.worktree_id, cx) - .is_some_and(|worktree| { + .map_or(false, |worktree| { worktree.read(cx).root_entry() == Some(&directory_entry.entry) }); let folded = auto_fold_dirs @@ -3667,7 +3678,7 @@ impl OutlinePanel { && outline_panel .unfolded_dirs .get(&directory_entry.worktree_id) - .is_none_or(|unfolded_dirs| { + .map_or(true, |unfolded_dirs| { !unfolded_dirs.contains(&directory_entry.entry.id) }); let fs_depth = outline_panel @@ -3747,7 +3758,7 @@ impl OutlinePanel { .iter() .rev() .nth(folded_dirs.entries.len() + 1) - .is_none_or(|parent| parent.expanded); + .map_or(true, |parent| parent.expanded); if start_of_collapsed_dir_sequence || parent_expanded || query.is_some() @@ -3807,7 +3818,7 @@ impl OutlinePanel { .iter() .all(|entry| entry.path != parent.path) }) - .is_none_or(|parent| parent.expanded); + .map_or(true, |parent| parent.expanded); if !is_singleton && (parent_expanded || query.is_some()) { outline_panel.push_entry( &mut generation_state, @@ -3832,7 +3843,7 @@ impl OutlinePanel { .iter() .all(|entry| entry.path != parent.path) }) - .is_none_or(|parent| parent.expanded); + .map_or(true, |parent| parent.expanded); if !is_singleton && (parent_expanded || query.is_some()) { outline_panel.push_entry( &mut generation_state, @@ -3910,19 +3921,19 @@ impl OutlinePanel { } else { None }; - if let Some((buffer_id, entry_excerpts)) = excerpts_to_consider - && !active_editor.read(cx).is_buffer_folded(buffer_id, cx) - { - outline_panel.add_excerpt_entries( - &mut generation_state, - buffer_id, - entry_excerpts, - depth, - track_matches, - is_singleton, - query.as_deref(), - cx, - ); + if let Some((buffer_id, entry_excerpts)) = excerpts_to_consider { + if !active_editor.read(cx).is_buffer_folded(buffer_id, cx) { + outline_panel.add_excerpt_entries( + &mut generation_state, + buffer_id, + entry_excerpts, + depth, + track_matches, + is_singleton, + query.as_deref(), + cx, + ); + } } } } @@ -3953,7 +3964,7 @@ impl OutlinePanel { .iter() .all(|entry| entry.path != parent.path) }) - .is_none_or(|parent| parent.expanded); + .map_or(true, |parent| parent.expanded); if parent_expanded || query.is_some() { outline_panel.push_entry( &mut generation_state, @@ -4393,16 +4404,15 @@ impl OutlinePanel { }) .filter(|(match_range, _)| { let editor = active_editor.read(cx); - let snapshot = editor.buffer().read(cx).snapshot(cx); - if let Some(buffer_id) = snapshot.buffer_id_for_anchor(match_range.start) - && editor.is_buffer_folded(buffer_id, cx) - { - return false; + if let Some(buffer_id) = match_range.start.buffer_id { + if editor.is_buffer_folded(buffer_id, cx) { + return false; + } } - if let Some(buffer_id) = snapshot.buffer_id_for_anchor(match_range.end) - && editor.is_buffer_folded(buffer_id, cx) - { - return false; + if let Some(buffer_id) = match_range.start.buffer_id { + if editor.is_buffer_folded(buffer_id, cx) { + return false; + } } true }); @@ -4434,7 +4444,7 @@ impl OutlinePanel { } fn should_replace_active_item(&self, new_active_item: &dyn ItemHandle) -> bool { - self.active_item().is_none_or(|active_item| { + self.active_item().map_or(true, |active_item| { !self.pinned && active_item.item_id() != new_active_item.item_id() }) } @@ -4446,14 +4456,16 @@ impl OutlinePanel { cx: &mut Context, ) { self.pinned = !self.pinned; - if !self.pinned - && let Some((active_item, active_editor)) = self + if !self.pinned { + if let Some((active_item, active_editor)) = self .workspace .upgrade() .and_then(|workspace| workspace_active_editor(workspace.read(cx), cx)) - && self.should_replace_active_item(active_item.as_ref()) - { - self.replace_active_editor(active_item, active_editor, window, cx); + { + if self.should_replace_active_item(active_item.as_ref()) { + self.replace_active_editor(active_item, active_editor, window, cx); + } + } } cx.notify(); @@ -4803,45 +4815,51 @@ impl OutlinePanel { .when(show_indent_guides, |list| { list.with_decoration( ui::indent_guides(px(indent_size), IndentGuideColors::panel(cx)) - .with_compute_indents_fn(cx.entity(), |outline_panel, range, _, _| { - let entries = outline_panel.cached_entries.get(range); - if let Some(entries) = entries { - entries.iter().map(|item| item.depth).collect() - } else { - smallvec::SmallVec::new() - } - }) - .with_render_fn(cx.entity(), move |outline_panel, params, _, _| { - const LEFT_OFFSET: Pixels = px(14.); + .with_compute_indents_fn( + cx.entity().clone(), + |outline_panel, range, _, _| { + let entries = outline_panel.cached_entries.get(range); + if let Some(entries) = entries { + entries.into_iter().map(|item| item.depth).collect() + } else { + smallvec::SmallVec::new() + } + }, + ) + .with_render_fn( + cx.entity().clone(), + move |outline_panel, params, _, _| { + const LEFT_OFFSET: Pixels = px(14.); - let indent_size = params.indent_size; - let item_height = params.item_height; - let active_indent_guide_ix = find_active_indent_guide_ix( - outline_panel, - ¶ms.indent_guides, - ); + let indent_size = params.indent_size; + let item_height = params.item_height; + let active_indent_guide_ix = find_active_indent_guide_ix( + outline_panel, + ¶ms.indent_guides, + ); - params - .indent_guides - .into_iter() - .enumerate() - .map(|(ix, layout)| { - let bounds = Bounds::new( - point( - layout.offset.x * indent_size + LEFT_OFFSET, - layout.offset.y * item_height, - ), - size(px(1.), layout.length * item_height), - ); - ui::RenderedIndentGuide { - bounds, - layout, - is_active: active_indent_guide_ix == Some(ix), - hitbox: None, - } - }) - .collect() - }), + params + .indent_guides + .into_iter() + .enumerate() + .map(|(ix, layout)| { + let bounds = Bounds::new( + point( + layout.offset.x * indent_size + LEFT_OFFSET, + layout.offset.y * item_height, + ), + size(px(1.), layout.length * item_height), + ); + ui::RenderedIndentGuide { + bounds, + layout, + is_active: active_indent_guide_ix == Some(ix), + hitbox: None, + } + }) + .collect() + }, + ), ) }) }; @@ -5055,23 +5073,24 @@ impl Panel for OutlinePanel { let old_active = outline_panel.active; outline_panel.active = active; if old_active != active { - if active - && let Some((active_item, active_editor)) = + if active { + if let Some((active_item, active_editor)) = outline_panel.workspace.upgrade().and_then(|workspace| { workspace_active_editor(workspace.read(cx), cx) }) - { - if outline_panel.should_replace_active_item(active_item.as_ref()) { - outline_panel.replace_active_editor( - active_item, - active_editor, - window, - cx, - ); - } else { - outline_panel.update_fs_entries(active_editor, None, window, cx) + { + if outline_panel.should_replace_active_item(active_item.as_ref()) { + outline_panel.replace_active_editor( + active_item, + active_editor, + window, + cx, + ); + } else { + outline_panel.update_fs_entries(active_editor, None, window, cx) + } + return; } - return; } if !outline_panel.pinned { @@ -5092,7 +5111,7 @@ impl Panel for OutlinePanel { impl Focusable for OutlinePanel { fn focus_handle(&self, cx: &App) -> FocusHandle { - self.filter_editor.focus_handle(cx) + self.filter_editor.focus_handle(cx).clone() } } @@ -5306,8 +5325,8 @@ fn subscribe_for_editor_events( }) .copied(), ); - if !ignore_selections_change - && let Some(entry_to_select) = latest_unfolded_buffer_id + if !ignore_selections_change { + if let Some(entry_to_select) = latest_unfolded_buffer_id .or(latest_folded_buffer_id) .and_then(|toggled_buffer_id| { outline_panel.fs_entries.iter().find_map( @@ -5331,15 +5350,16 @@ fn subscribe_for_editor_events( ) }) .map(PanelEntry::Fs) - { - outline_panel.select_entry(entry_to_select, true, window, cx); + { + outline_panel.select_entry(entry_to_select, true, window, cx); + } } outline_panel.update_fs_entries(editor.clone(), debounce, window, cx); } EditorEvent::Reparsed(buffer_id) => { if let Some(excerpts) = outline_panel.excerpts.get_mut(buffer_id) { - for excerpt in excerpts.values_mut() { + for (_, excerpt) in excerpts { excerpt.invalidate_outlines(); } } @@ -5484,7 +5504,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -5500,7 +5520,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -5518,7 +5538,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -5555,7 +5575,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -5569,7 +5589,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -5588,7 +5608,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -5616,7 +5636,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -5704,7 +5724,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, None, cx, @@ -5727,7 +5747,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, None, cx, @@ -5753,7 +5773,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, None, cx, @@ -5859,7 +5879,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -5882,7 +5902,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -5919,7 +5939,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -5938,7 +5958,7 @@ mod tests { }); outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.open_excerpts(&editor::actions::OpenExcerpts, window, cx); + outline_panel.open_excerpts(&editor::OpenExcerpts, window, cx); }); cx.executor() .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); @@ -5956,7 +5976,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6059,7 +6079,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6085,7 +6105,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6109,7 +6129,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6130,7 +6150,7 @@ mod tests { assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6218,7 +6238,7 @@ struct OutlineEntryExcerpt { assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6245,7 +6265,7 @@ outline: struct OutlineEntryExcerpt assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6272,7 +6292,7 @@ outline: struct OutlineEntryExcerpt <==== selected assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6299,7 +6319,7 @@ outline: struct OutlineEntryExcerpt assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6326,7 +6346,7 @@ outline: struct OutlineEntryExcerpt assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6353,7 +6373,7 @@ outline: struct OutlineEntryExcerpt assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6380,7 +6400,7 @@ outline: struct OutlineEntryExcerpt <==== selected assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6407,7 +6427,7 @@ outline: struct OutlineEntryExcerpt assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6434,7 +6454,7 @@ outline: struct OutlineEntryExcerpt assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6461,7 +6481,7 @@ outline: struct OutlineEntryExcerpt assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6488,7 +6508,7 @@ outline: struct OutlineEntryExcerpt <==== selected assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6594,7 +6614,7 @@ outline: struct OutlineEntryExcerpt assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6631,7 +6651,7 @@ outline: struct OutlineEntryExcerpt assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6659,7 +6679,7 @@ outline: struct OutlineEntryExcerpt assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6691,7 +6711,7 @@ outline: struct OutlineEntryExcerpt assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6722,7 +6742,7 @@ outline: struct OutlineEntryExcerpt assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -6850,7 +6870,7 @@ outline: struct OutlineEntryExcerpt .render_data .get_or_init(|| SearchData::new( &search_entry.match_range, - multi_buffer_snapshot + &multi_buffer_snapshot )) .context_text ) @@ -7241,7 +7261,7 @@ outline: struct OutlineEntryExcerpt assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -7300,7 +7320,7 @@ outline: fn main()" assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -7324,7 +7344,7 @@ outline: fn main()" assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -7389,7 +7409,7 @@ outline: fn main()" assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -7530,7 +7550,7 @@ outline: fn main()" assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -7568,7 +7588,7 @@ outline: fn main()" assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -7602,7 +7622,7 @@ outline: fn main()" assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, @@ -7634,7 +7654,7 @@ outline: fn main()" assert_eq!( display_entries( &project, - &snapshot(outline_panel, cx), + &snapshot(&outline_panel, cx), &outline_panel.cached_entries, outline_panel.selected_entry(), cx, diff --git a/crates/panel/src/panel.rs b/crates/panel/src/panel.rs index 1930f654e9..658a51167b 100644 --- a/crates/panel/src/panel.rs +++ b/crates/panel/src/panel.rs @@ -52,7 +52,7 @@ impl RenderOnce for PanelTab { pub fn panel_button(label: impl Into) -> ui::Button { let label = label.into(); - let id = ElementId::Name(label.to_lowercase().replace(' ', "_").into()); + let id = ElementId::Name(label.clone().to_lowercase().replace(' ', "_").into()); ui::Button::new(id, label) .label_size(ui::LabelSize::Small) .icon_size(ui::IconSize::Small) diff --git a/crates/paths/src/paths.rs b/crates/paths/src/paths.rs index aab0354c96..47a0f12c06 100644 --- a/crates/paths/src/paths.rs +++ b/crates/paths/src/paths.rs @@ -41,7 +41,7 @@ pub fn remote_server_dir_relative() -> &'static Path { /// # Arguments /// /// * `dir` - The path to use as the custom data directory. This will be used as the base -/// directory for all user data, including databases, extensions, and logs. +/// directory for all user data, including databases, extensions, and logs. /// /// # Returns /// diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 34af5fed02..692bdd5bd7 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -292,7 +292,7 @@ impl Picker { window: &mut Window, cx: &mut Context, ) -> Self { - let element_container = Self::create_element_container(container); + let element_container = Self::create_element_container(container, cx); let scrollbar_state = match &element_container { ElementContainer::UniformList(scroll_handle) => { ScrollbarState::new(scroll_handle.clone()) @@ -323,13 +323,31 @@ impl Picker { this } - fn create_element_container(container: ContainerKind) -> ElementContainer { + fn create_element_container( + container: ContainerKind, + cx: &mut Context, + ) -> ElementContainer { match container { ContainerKind::UniformList => { ElementContainer::UniformList(UniformListScrollHandle::new()) } ContainerKind::List => { - ElementContainer::List(ListState::new(0, gpui::ListAlignment::Top, px(1000.))) + let entity = cx.entity().downgrade(); + ElementContainer::List(ListState::new( + 0, + gpui::ListAlignment::Top, + px(1000.), + move |ix, window, cx| { + entity + .upgrade() + .map(|entity| { + entity.update(cx, |this, cx| { + this.render_element(window, cx, ix).into_any_element() + }) + }) + .unwrap_or_else(|| div().into_any_element()) + }, + )) } } } @@ -768,16 +786,11 @@ impl Picker { .py_1() .track_scroll(scroll_handle.clone()) .into_any_element(), - ElementContainer::List(state) => list( - state.clone(), - cx.processor(|this, ix, window, cx| { - this.render_element(window, cx, ix).into_any_element() - }), - ) - .with_sizing_behavior(sizing_behavior) - .flex_grow() - .py_2() - .into_any_element(), + ElementContainer::List(state) => list(state.clone()) + .with_sizing_behavior(sizing_behavior) + .flex_grow() + .py_2() + .into_any_element(), } } diff --git a/crates/picker/src/popover_menu.rs b/crates/picker/src/popover_menu.rs index baf0918fd6..dd1d9c2865 100644 --- a/crates/picker/src/popover_menu.rs +++ b/crates/picker/src/popover_menu.rs @@ -80,12 +80,11 @@ where { fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { let picker = self.picker.clone(); - PopoverMenu::new("popover-menu") .menu(move |_window, _cx| Some(picker.clone())) .trigger_with_tooltip(self.trigger, self.tooltip) .anchor(self.anchor) - .when_some(self.handle, |menu, handle| menu.with_handle(handle)) + .when_some(self.handle.clone(), |menu, handle| menu.with_handle(handle)) .offset(gpui::Point { x: px(0.0), y: px(-2.0), diff --git a/crates/prettier/src/prettier.rs b/crates/prettier/src/prettier.rs index 32e39d466f..33320e6845 100644 --- a/crates/prettier/src/prettier.rs +++ b/crates/prettier/src/prettier.rs @@ -119,7 +119,7 @@ impl Prettier { None } }).any(|workspace_definition| { - workspace_definition == subproject_path.to_string_lossy() || PathMatcher::new(&[workspace_definition]).ok().is_some_and(|path_matcher| path_matcher.is_match(subproject_path)) + workspace_definition == subproject_path.to_string_lossy() || PathMatcher::new(&[workspace_definition]).ok().map_or(false, |path_matcher| path_matcher.is_match(subproject_path)) }) { anyhow::ensure!(has_prettier_in_node_modules(fs, &path_to_check).await?, "Path {path_to_check:?} is the workspace root for project in {closest_package_json_path:?}, but it has no prettier installed"); log::info!("Found prettier path {path_to_check:?} in the workspace root for project in {closest_package_json_path:?}"); @@ -185,11 +185,11 @@ impl Prettier { .metadata(&ignore_path) .await .with_context(|| format!("fetching metadata for {ignore_path:?}"))? - && !metadata.is_dir - && !metadata.is_symlink { - log::info!("Found prettier ignore at {ignore_path:?}"); - return Ok(ControlFlow::Continue(Some(path_to_check))); + if !metadata.is_dir && !metadata.is_symlink { + log::info!("Found prettier ignore at {ignore_path:?}"); + return Ok(ControlFlow::Continue(Some(path_to_check))); + } } match &closest_package_json_path { None => closest_package_json_path = Some(path_to_check.clone()), @@ -217,19 +217,19 @@ impl Prettier { workspace_definition == subproject_path.to_string_lossy() || PathMatcher::new(&[workspace_definition]) .ok() - .is_some_and(|path_matcher| { + .map_or(false, |path_matcher| { path_matcher.is_match(subproject_path) }) }) { let workspace_ignore = path_to_check.join(".prettierignore"); - if let Some(metadata) = fs.metadata(&workspace_ignore).await? - && !metadata.is_dir - { - log::info!( - "Found prettier ignore at workspace root {workspace_ignore:?}" - ); - return Ok(ControlFlow::Continue(Some(path_to_check))); + if let Some(metadata) = fs.metadata(&workspace_ignore).await? { + if !metadata.is_dir { + log::info!( + "Found prettier ignore at workspace root {workspace_ignore:?}" + ); + return Ok(ControlFlow::Continue(Some(path_to_check))); + } } } } @@ -549,16 +549,18 @@ async fn read_package_json( .metadata(&possible_package_json) .await .with_context(|| format!("fetching metadata for package json {possible_package_json:?}"))? - && !package_json_metadata.is_dir - && !package_json_metadata.is_symlink { - let package_json_contents = fs - .load(&possible_package_json) - .await - .with_context(|| format!("reading {possible_package_json:?} file contents"))?; - return serde_json::from_str::>(&package_json_contents) + if !package_json_metadata.is_dir && !package_json_metadata.is_symlink { + let package_json_contents = fs + .load(&possible_package_json) + .await + .with_context(|| format!("reading {possible_package_json:?} file contents"))?; + return serde_json::from_str::>( + &package_json_contents, + ) .map(Some) .with_context(|| format!("parsing {possible_package_json:?} file contents")); + } } Ok(None) } diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index 295bad6e59..b8101e14f3 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -88,18 +88,9 @@ pub enum BufferStoreEvent { }, } -#[derive(Default, Debug, Clone)] +#[derive(Default, Debug)] pub struct ProjectTransaction(pub HashMap, language::Transaction>); -impl PartialEq for ProjectTransaction { - fn eq(&self, other: &Self) -> bool { - self.0.len() == other.0.len() - && self.0.iter().all(|(buffer, transaction)| { - other.0.get(buffer).is_some_and(|t| t.id == transaction.id) - }) - } -} - impl EventEmitter for BufferStore {} impl RemoteBufferStore { @@ -177,7 +168,7 @@ impl RemoteBufferStore { .with_context(|| { format!("no worktree found for id {}", file.worktree_id) })?; - buffer_file = Some(Arc::new(File::from_proto(file, worktree, cx)?) + buffer_file = Some(Arc::new(File::from_proto(file, worktree.clone(), cx)?) as Arc); } Buffer::from_proto(replica_id, capability, state, buffer_file) @@ -243,7 +234,7 @@ impl RemoteBufferStore { } } } - Ok(None) + return Ok(None); } pub fn incomplete_buffer_ids(&self) -> Vec { @@ -422,10 +413,13 @@ impl LocalBufferStore { cx: &mut Context, ) { cx.subscribe(worktree, |this, worktree, event, cx| { - if worktree.read(cx).is_local() - && let worktree::Event::UpdatedEntries(changes) = event - { - Self::local_worktree_entries_changed(this, &worktree, changes, cx); + if worktree.read(cx).is_local() { + match event { + worktree::Event::UpdatedEntries(changes) => { + Self::local_worktree_entries_changed(this, &worktree, changes, cx); + } + _ => {} + } } }) .detach(); @@ -600,7 +594,7 @@ impl LocalBufferStore { else { return Task::ready(Err(anyhow!("no such worktree"))); }; - self.save_local_buffer(buffer, worktree, path.path, true, cx) + self.save_local_buffer(buffer, worktree, path.path.clone(), true, cx) } fn open_buffer( @@ -854,7 +848,7 @@ impl BufferStore { ) -> Task> { match &mut self.state { BufferStoreState::Local(this) => this.save_buffer(buffer, cx), - BufferStoreState::Remote(this) => this.save_remote_buffer(buffer, None, cx), + BufferStoreState::Remote(this) => this.save_remote_buffer(buffer.clone(), None, cx), } } @@ -953,9 +947,10 @@ impl BufferStore { } pub fn get_by_path(&self, path: &ProjectPath) -> Option> { - self.path_to_buffer_id - .get(path) - .and_then(|buffer_id| self.get(*buffer_id)) + self.path_to_buffer_id.get(path).and_then(|buffer_id| { + let buffer = self.get(*buffer_id); + buffer + }) } pub fn get(&self, buffer_id: BufferId) -> Option> { @@ -1099,10 +1094,10 @@ impl BufferStore { .collect::>() })?; for buffer_task in buffers { - if let Some(buffer) = buffer_task.await.log_err() - && tx.send(buffer).await.is_err() - { - return anyhow::Ok(()); + if let Some(buffer) = buffer_task.await.log_err() { + if tx.send(buffer).await.is_err() { + return anyhow::Ok(()); + } } } } @@ -1147,7 +1142,7 @@ impl BufferStore { envelope: TypedEnvelope, mut cx: AsyncApp, ) -> Result { - let payload = envelope.payload; + let payload = envelope.payload.clone(); let buffer_id = BufferId::new(payload.buffer_id)?; let ops = payload .operations @@ -1178,11 +1173,11 @@ impl BufferStore { buffer_id: BufferId, handle: OpenLspBufferHandle, ) { - if let Some(shared_buffers) = self.shared_buffers.get_mut(&peer_id) - && let Some(buffer) = shared_buffers.get_mut(&buffer_id) - { - buffer.lsp_handle = Some(handle); - return; + if let Some(shared_buffers) = self.shared_buffers.get_mut(&peer_id) { + if let Some(buffer) = shared_buffers.get_mut(&buffer_id) { + buffer.lsp_handle = Some(handle); + return; + } } debug_panic!("tried to register shared lsp handle, but buffer was not shared") } @@ -1318,7 +1313,10 @@ impl BufferStore { let new_path = file.path.clone(); buffer.file_updated(Arc::new(file), cx); - if old_file.as_ref().is_none_or(|old| *old.path() != new_path) { + if old_file + .as_ref() + .map_or(true, |old| *old.path() != new_path) + { Some(old_file) } else { None @@ -1347,7 +1345,7 @@ impl BufferStore { mut cx: AsyncApp, ) -> Result { let buffer_id = BufferId::new(envelope.payload.buffer_id)?; - let (buffer, project_id) = this.read_with(&cx, |this, _| { + let (buffer, project_id) = this.read_with(&mut cx, |this, _| { anyhow::Ok(( this.get_existing(buffer_id)?, this.downstream_client @@ -1361,7 +1359,7 @@ impl BufferStore { buffer.wait_for_version(deserialize_version(&envelope.payload.version)) })? .await?; - let buffer_id = buffer.read_with(&cx, |buffer, _| buffer.remote_id())?; + let buffer_id = buffer.read_with(&mut cx, |buffer, _| buffer.remote_id())?; if let Some(new_path) = envelope.payload.new_path { let new_path = ProjectPath::from_proto(new_path); @@ -1374,7 +1372,7 @@ impl BufferStore { .await?; } - buffer.read_with(&cx, |buffer, _| proto::BufferSaved { + buffer.read_with(&mut cx, |buffer, _| proto::BufferSaved { project_id, buffer_id: buffer_id.into(), version: serialize_version(buffer.saved_version()), @@ -1390,14 +1388,14 @@ impl BufferStore { let peer_id = envelope.sender_id; let buffer_id = BufferId::new(envelope.payload.buffer_id)?; this.update(&mut cx, |this, cx| { - if let Some(shared) = this.shared_buffers.get_mut(&peer_id) - && shared.remove(&buffer_id).is_some() - { - cx.emit(BufferStoreEvent::SharedBufferClosed(peer_id, buffer_id)); - if shared.is_empty() { - this.shared_buffers.remove(&peer_id); + if let Some(shared) = this.shared_buffers.get_mut(&peer_id) { + if shared.remove(&buffer_id).is_some() { + cx.emit(BufferStoreEvent::SharedBufferClosed(peer_id, buffer_id)); + if shared.is_empty() { + this.shared_buffers.remove(&peer_id); + } + return; } - return; } debug_panic!( "peer_id {} closed buffer_id {} which was either not open or already closed", diff --git a/crates/project/src/color_extractor.rs b/crates/project/src/color_extractor.rs index 6e9907e30b..5473da88af 100644 --- a/crates/project/src/color_extractor.rs +++ b/crates/project/src/color_extractor.rs @@ -4,8 +4,8 @@ use gpui::{Hsla, Rgba}; use lsp::{CompletionItem, Documentation}; use regex::{Regex, RegexBuilder}; -const HEX: &str = r#"(#(?:[\da-fA-F]{3}){1,2})"#; -const RGB_OR_HSL: &str = r#"(rgba?|hsla?)\(\s*(\d{1,3}%?)\s*,\s*(\d{1,3}%?)\s*,\s*(\d{1,3}%?)\s*(?:,\s*(1|0?\.\d+))?\s*\)"#; +const HEX: &'static str = r#"(#(?:[\da-fA-F]{3}){1,2})"#; +const RGB_OR_HSL: &'static str = r#"(rgba?|hsla?)\(\s*(\d{1,3}%?)\s*,\s*(\d{1,3}%?)\s*,\s*(\d{1,3}%?)\s*(?:,\s*(1|0?\.\d+))?\s*\)"#; static RELAXED_HEX_REGEX: LazyLock = LazyLock::new(|| { RegexBuilder::new(HEX) @@ -102,7 +102,7 @@ fn parse(str: &str, mode: ParseMode) -> Option { }; } - None + return None; } fn parse_component(value: &str, max: f32) -> Option { @@ -141,7 +141,7 @@ mod tests { use gpui::rgba; use lsp::{CompletionItem, CompletionItemKind}; - pub const COLOR_TABLE: &[(&str, Option)] = &[ + pub const COLOR_TABLE: &[(&'static str, Option)] = &[ // -- Invalid -- // Invalid hex ("f0f", None), diff --git a/crates/project/src/context_server_store.rs b/crates/project/src/context_server_store.rs index 49a430c261..c96ab4e8f3 100644 --- a/crates/project/src/context_server_store.rs +++ b/crates/project/src/context_server_store.rs @@ -368,7 +368,7 @@ impl ContextServerStore { } pub fn restart_server(&mut self, id: &ContextServerId, cx: &mut Context) -> Result<()> { - if let Some(state) = self.servers.get(id) { + if let Some(state) = self.servers.get(&id) { let configuration = state.configuration(); self.stop_server(&state.server().id(), cx)?; @@ -397,8 +397,9 @@ impl ContextServerStore { let server = server.clone(); let configuration = configuration.clone(); async move |this, cx| { - match server.clone().start(cx).await { + match server.clone().start(&cx).await { Ok(_) => { + log::info!("Started {} context server", id); debug_assert!(server.client().is_some()); this.update(cx, |this, cx| { @@ -587,7 +588,7 @@ impl ContextServerStore { for server_id in this.servers.keys() { // All servers that are not in desired_servers should be removed from the store. // This can happen if the user removed a server from the context server settings. - if !configured_servers.contains_key(server_id) { + if !configured_servers.contains_key(&server_id) { if disabled_servers.contains_key(&server_id.0) { servers_to_stop.insert(server_id.clone()); } else { @@ -641,8 +642,8 @@ mod tests { #[gpui::test] async fn test_context_server_status(cx: &mut TestAppContext) { - const SERVER_1_ID: &str = "mcp-1"; - const SERVER_2_ID: &str = "mcp-2"; + const SERVER_1_ID: &'static str = "mcp-1"; + const SERVER_2_ID: &'static str = "mcp-2"; let (_fs, project) = setup_context_server_test( cx, @@ -721,8 +722,8 @@ mod tests { #[gpui::test] async fn test_context_server_status_events(cx: &mut TestAppContext) { - const SERVER_1_ID: &str = "mcp-1"; - const SERVER_2_ID: &str = "mcp-2"; + const SERVER_1_ID: &'static str = "mcp-1"; + const SERVER_2_ID: &'static str = "mcp-2"; let (_fs, project) = setup_context_server_test( cx, @@ -760,7 +761,7 @@ mod tests { &store, vec![ (server_1_id.clone(), ContextServerStatus::Starting), - (server_1_id, ContextServerStatus::Running), + (server_1_id.clone(), ContextServerStatus::Running), (server_2_id.clone(), ContextServerStatus::Starting), (server_2_id.clone(), ContextServerStatus::Running), (server_2_id.clone(), ContextServerStatus::Stopped), @@ -783,7 +784,7 @@ mod tests { #[gpui::test(iterations = 25)] async fn test_context_server_concurrent_starts(cx: &mut TestAppContext) { - const SERVER_1_ID: &str = "mcp-1"; + const SERVER_1_ID: &'static str = "mcp-1"; let (_fs, project) = setup_context_server_test( cx, @@ -844,8 +845,8 @@ mod tests { #[gpui::test] async fn test_context_server_maintain_servers_loop(cx: &mut TestAppContext) { - const SERVER_1_ID: &str = "mcp-1"; - const SERVER_2_ID: &str = "mcp-2"; + const SERVER_1_ID: &'static str = "mcp-1"; + const SERVER_2_ID: &'static str = "mcp-2"; let server_1_id = ContextServerId(SERVER_1_ID.into()); let server_2_id = ContextServerId(SERVER_2_ID.into()); @@ -1083,7 +1084,7 @@ mod tests { #[gpui::test] async fn test_context_server_enabled_disabled(cx: &mut TestAppContext) { - const SERVER_1_ID: &str = "mcp-1"; + const SERVER_1_ID: &'static str = "mcp-1"; let server_1_id = ContextServerId(SERVER_1_ID.into()); diff --git a/crates/project/src/context_server_store/extension.rs b/crates/project/src/context_server_store/extension.rs index 2a3a0c2e4b..1eb0fe7da1 100644 --- a/crates/project/src/context_server_store/extension.rs +++ b/crates/project/src/context_server_store/extension.rs @@ -63,7 +63,7 @@ impl registry::ContextServerDescriptor for ContextServerDescriptor { .await?; command.command = extension.path_from_extension(&command.command); - log::debug!("loaded command for context server {id}: {command:?}"); + log::info!("loaded command for context server {id}: {command:?}"); Ok(ContextServerCommand { path: command.command, diff --git a/crates/project/src/debugger.rs b/crates/project/src/debugger.rs index 0bf6a0d61b..6c22468040 100644 --- a/crates/project/src/debugger.rs +++ b/crates/project/src/debugger.rs @@ -6,9 +6,9 @@ //! //! There are few reasons for this divide: //! - Breakpoints persist across debug sessions and they're not really specific to any particular session. Sure, we have to send protocol messages for them -//! (so they're a "thing" in the protocol), but we also want to set them before any session starts up. +//! (so they're a "thing" in the protocol), but we also want to set them before any session starts up. //! - Debug clients are doing the heavy lifting, and this is where UI grabs all of it's data from. They also rely on breakpoint store during initialization to obtain -//! current set of breakpoints. +//! current set of breakpoints. //! - Since DAP store knows about all of the available debug sessions, it is responsible for routing RPC requests to sessions. It also knows how to find adapters for particular kind of session. pub mod breakpoint_store; diff --git a/crates/project/src/debugger/breakpoint_store.rs b/crates/project/src/debugger/breakpoint_store.rs index c47e5d35d5..025dca4100 100644 --- a/crates/project/src/debugger/breakpoint_store.rs +++ b/crates/project/src/debugger/breakpoint_store.rs @@ -192,7 +192,7 @@ impl BreakpointStore { } pub(crate) fn shared(&mut self, project_id: u64, downstream_client: AnyProtoClient) { - self.downstream_client = Some((downstream_client, project_id)); + self.downstream_client = Some((downstream_client.clone(), project_id)); } pub(crate) fn unshared(&mut self, cx: &mut Context) { @@ -267,7 +267,7 @@ impl BreakpointStore { message: TypedEnvelope, mut cx: AsyncApp, ) -> Result { - let breakpoints = this.read_with(&cx, |this, _| this.breakpoint_store())?; + let breakpoints = this.read_with(&mut cx, |this, _| this.breakpoint_store())?; let path = this .update(&mut cx, |this, cx| { this.project_path_for_absolute_path(message.payload.path.as_ref(), cx) @@ -317,8 +317,8 @@ impl BreakpointStore { .iter() .filter_map(|breakpoint| { breakpoint.bp.bp.to_proto( - path, - breakpoint.position(), + &path, + &breakpoint.position(), &breakpoint.session_state, ) }) @@ -450,9 +450,9 @@ impl BreakpointStore { }); if let Some(found_bp) = found_bp { - found_bp.message = Some(log_message); + found_bp.message = Some(log_message.clone()); } else { - breakpoint.bp.message = Some(log_message); + breakpoint.bp.message = Some(log_message.clone()); // We did not remove any breakpoint, hence let's toggle one. breakpoint_set .breakpoints @@ -482,9 +482,9 @@ impl BreakpointStore { }); if let Some(found_bp) = found_bp { - found_bp.hit_condition = Some(hit_condition); + found_bp.hit_condition = Some(hit_condition.clone()); } else { - breakpoint.bp.hit_condition = Some(hit_condition); + breakpoint.bp.hit_condition = Some(hit_condition.clone()); // We did not remove any breakpoint, hence let's toggle one. breakpoint_set .breakpoints @@ -514,9 +514,9 @@ impl BreakpointStore { }); if let Some(found_bp) = found_bp { - found_bp.condition = Some(condition); + found_bp.condition = Some(condition.clone()); } else { - breakpoint.bp.condition = Some(condition); + breakpoint.bp.condition = Some(condition.clone()); // We did not remove any breakpoint, hence let's toggle one. breakpoint_set .breakpoints @@ -591,7 +591,7 @@ impl BreakpointStore { cx: &mut Context, ) { if let Some(breakpoints) = self.breakpoints.remove(&old_path) { - self.breakpoints.insert(new_path, breakpoints); + self.breakpoints.insert(new_path.clone(), breakpoints); cx.notify(); } @@ -623,11 +623,12 @@ impl BreakpointStore { file_breakpoints.breakpoints.iter().filter_map({ let range = range.clone(); move |bp| { - if let Some(range) = &range - && (bp.position().cmp(&range.start, buffer_snapshot).is_lt() - || bp.position().cmp(&range.end, buffer_snapshot).is_gt()) - { - return None; + if let Some(range) = &range { + if bp.position().cmp(&range.start, buffer_snapshot).is_lt() + || bp.position().cmp(&range.end, buffer_snapshot).is_gt() + { + return None; + } } let session_state = active_session_id .and_then(|id| bp.session_state.get(&id)) @@ -752,7 +753,7 @@ impl BreakpointStore { .iter() .map(|breakpoint| { let position = snapshot - .summary_for_anchor::(breakpoint.position()) + .summary_for_anchor::(&breakpoint.position()) .row; let breakpoint = &breakpoint.bp; SourceBreakpoint { @@ -831,6 +832,7 @@ impl BreakpointStore { new_breakpoints.insert(path, breakpoints_for_file); } this.update(cx, |this, cx| { + log::info!("Finish deserializing breakpoints & initializing breakpoint store"); for (path, count) in new_breakpoints.iter().map(|(path, bp_in_file)| { (path.to_string_lossy(), bp_in_file.breakpoints.len()) }) { @@ -904,7 +906,7 @@ impl BreakpointState { } #[inline] - pub fn to_int(self) -> i32 { + pub fn to_int(&self) -> i32 { match self { BreakpointState::Enabled => 0, BreakpointState::Disabled => 1, diff --git a/crates/project/src/debugger/dap_command.rs b/crates/project/src/debugger/dap_command.rs index 772ff2dcfe..3be3192369 100644 --- a/crates/project/src/debugger/dap_command.rs +++ b/crates/project/src/debugger/dap_command.rs @@ -1454,7 +1454,7 @@ impl DapCommand for EvaluateCommand { variables_reference: message.variable_reference, named_variables: message.named_variables, indexed_variables: message.indexed_variables, - memory_reference: message.memory_reference, + memory_reference: message.memory_reference.clone(), value_location_reference: None, //TODO }) } diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 2906c32ff4..6f834b5dc0 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -34,7 +34,7 @@ use http_client::HttpClient; use language::{Buffer, LanguageToolchainStore, language_settings::InlayHintKind}; use node_runtime::NodeRuntime; -use remote::{SshInfo, SshRemoteClient, ssh_session::SshArgs}; +use remote::{SshRemoteClient, ssh_session::SshArgs}; use rpc::{ AnyProtoClient, TypedEnvelope, proto::{self}, @@ -215,7 +215,7 @@ impl DapStore { dap_settings.and_then(|s| s.binary.as_ref().map(PathBuf::from)); let user_args = dap_settings.map(|s| s.args.clone()); - let delegate = self.delegate(worktree, console, cx); + let delegate = self.delegate(&worktree, console, cx); let cwd: Arc = worktree.read(cx).abs_path().as_ref().into(); cx.spawn(async move |this, cx| { @@ -254,18 +254,14 @@ impl DapStore { cx.spawn(async move |_, cx| { let response = request.await?; let binary = DebugAdapterBinary::from_proto(response)?; - let (mut ssh_command, envs, path_style, ssh_shell) = + let (mut ssh_command, envs, path_style) = ssh_client.read_with(cx, |ssh, _| { - let SshInfo { - args: SshArgs { arguments, envs }, - path_style, - shell, - } = ssh.ssh_info().context("SSH arguments not found")?; + let (SshArgs { arguments, envs }, path_style) = + ssh.ssh_info().context("SSH arguments not found")?; anyhow::Ok(( SshCommand { arguments }, envs.unwrap_or_default(), path_style, - shell, )) })??; @@ -284,7 +280,6 @@ impl DapStore { } let (program, args) = wrap_for_ssh( - &ssh_shell, &ssh_command, binary .command @@ -475,8 +470,9 @@ impl DapStore { session_id: impl Borrow, ) -> Option> { let session_id = session_id.borrow(); + let client = self.sessions.get(session_id).cloned(); - self.sessions.get(session_id).cloned() + client } pub fn sessions(&self) -> impl Iterator> { self.sessions.values() @@ -689,7 +685,7 @@ impl DapStore { let shutdown_id = parent_session.update(cx, |parent_session, _| { parent_session.remove_child_session_id(session_id); - if parent_session.child_session_ids().is_empty() { + if parent_session.child_session_ids().len() == 0 { Some(parent_session.session_id()) } else { None @@ -706,7 +702,7 @@ impl DapStore { cx.emit(DapStoreEvent::DebugClientShutdown(session_id)); cx.background_spawn(async move { - if !shutdown_children.is_empty() { + if shutdown_children.len() > 0 { let _ = join_all(shutdown_children).await; } @@ -726,7 +722,7 @@ impl DapStore { downstream_client: AnyProtoClient, _: &mut Context, ) { - self.downstream_client = Some((downstream_client, project_id)); + self.downstream_client = Some((downstream_client.clone(), project_id)); } pub fn unshared(&mut self, cx: &mut Context) { @@ -906,7 +902,7 @@ impl dap::adapters::DapDelegate for DapAdapterDelegate { } fn worktree_root_path(&self) -> &Path { - self.worktree.abs_path() + &self.worktree.abs_path() } fn http_client(&self) -> Arc { self.http_client.clone() diff --git a/crates/project/src/debugger/locators/cargo.rs b/crates/project/src/debugger/locators/cargo.rs index b2f9580f9c..fa265dae58 100644 --- a/crates/project/src/debugger/locators/cargo.rs +++ b/crates/project/src/debugger/locators/cargo.rs @@ -117,7 +117,7 @@ impl DapLocator for CargoLocator { .cwd .clone() .context("Couldn't get cwd from debug config which is needed for locators")?; - let builder = ShellBuilder::new(None, &build_config.shell).non_interactive(); + let builder = ShellBuilder::new(true, &build_config.shell).non_interactive(); let (program, args) = builder.build( Some("cargo".into()), &build_config @@ -126,7 +126,7 @@ impl DapLocator for CargoLocator { .cloned() .take_while(|arg| arg != "--") .chain(Some("--message-format=json".to_owned())) - .collect::>(), + .collect(), ); let mut child = util::command::new_smol_command(program) .args(args) @@ -146,7 +146,7 @@ impl DapLocator for CargoLocator { let is_test = build_config .args .first() - .is_some_and(|arg| arg == "test" || arg == "t"); + .map_or(false, |arg| arg == "test" || arg == "t"); let executables = output .lines() @@ -187,12 +187,12 @@ impl DapLocator for CargoLocator { .cloned(); } let executable = { - if let Some(name) = test_name.as_ref().and_then(|name| { + if let Some(ref name) = test_name.as_ref().and_then(|name| { name.strip_prefix('$') .map(|name| build_config.env.get(name)) .unwrap_or(Some(name)) }) { - find_best_executable(&executables, name).await + find_best_executable(&executables, &name).await } else { None } diff --git a/crates/project/src/debugger/locators/go.rs b/crates/project/src/debugger/locators/go.rs index eec06084ec..61436fce8f 100644 --- a/crates/project/src/debugger/locators/go.rs +++ b/crates/project/src/debugger/locators/go.rs @@ -174,7 +174,7 @@ impl DapLocator for GoLocator { request: "launch".to_string(), mode: "test".to_string(), program, - args, + args: args, build_flags, cwd: build_config.cwd.clone(), env: build_config.env.clone(), @@ -185,7 +185,7 @@ impl DapLocator for GoLocator { label: resolved_label.to_string().into(), adapter: adapter.0.clone(), build: None, - config, + config: config, tcp_connection: None, }) } @@ -220,7 +220,7 @@ impl DapLocator for GoLocator { request: "launch".to_string(), mode: "debug".to_string(), program, - args, + args: args, build_flags, }) .unwrap(); diff --git a/crates/project/src/debugger/locators/python.rs b/crates/project/src/debugger/locators/python.rs index 71efbb75b9..3de1281aed 100644 --- a/crates/project/src/debugger/locators/python.rs +++ b/crates/project/src/debugger/locators/python.rs @@ -28,7 +28,9 @@ impl DapLocator for PythonLocator { let valid_program = build_config.command.starts_with("$ZED_") || Path::new(&build_config.command) .file_name() - .is_some_and(|name| name.to_str().is_some_and(|path| path.starts_with("python"))); + .map_or(false, |name| { + name.to_str().is_some_and(|path| path.starts_with("python")) + }); if !valid_program || build_config.args.iter().any(|arg| arg == "-c") { // We cannot debug selections. return None; diff --git a/crates/project/src/debugger/memory.rs b/crates/project/src/debugger/memory.rs index 42ad64e688..fec3c344c5 100644 --- a/crates/project/src/debugger/memory.rs +++ b/crates/project/src/debugger/memory.rs @@ -3,7 +3,6 @@ //! Each byte in memory can either be mapped or unmapped. We try to mimic that twofold: //! - We assume that the memory is divided into pages of a fixed size. //! - We assume that each page can be either mapped or unmapped. -//! //! These two assumptions drive the shape of the memory representation. //! In particular, we want the unmapped pages to be represented without allocating any memory, as *most* //! of the memory in a program space is usually unmapped. @@ -166,8 +165,8 @@ impl Memory { /// - If it succeeds/fails wholesale, cool; we have no unknown memory regions in this page. /// - If it succeeds partially, we know # of mapped bytes. /// We might also know the # of unmapped bytes. -/// /// However, we're still unsure about what's *after* the unreadable region. +/// /// This is where this builder comes in. It lets us track the state of figuring out contents of a single page. pub(super) struct MemoryPageBuilder { chunks: MappedPageContents, @@ -319,18 +318,19 @@ impl Iterator for MemoryIterator { return None; } if let Some((current_page_address, current_memory_chunk)) = self.current_known_page.as_mut() - && current_page_address.0 <= self.start { - if let Some(next_cell) = current_memory_chunk.next() { - self.start += 1; - return Some(next_cell); - } else { - self.current_known_page.take(); + if current_page_address.0 <= self.start { + if let Some(next_cell) = current_memory_chunk.next() { + self.start += 1; + return Some(next_cell); + } else { + self.current_known_page.take(); + } } } if !self.fetch_next_page() { self.start += 1; - Some(MemoryCell(None)) + return Some(MemoryCell(None)); } else { self.next() } diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 81cb3ade2e..f60a7becf7 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -56,7 +56,7 @@ use std::{ }; use task::TaskContext; use text::{PointUtf16, ToPointUtf16}; -use util::{ResultExt, debug_panic, maybe}; +use util::{ResultExt, maybe}; use worktree::Worktree; #[derive(Debug, Copy, Clone, Hash, PartialEq, PartialOrd, Ord, Eq)] @@ -141,10 +141,7 @@ pub struct DataBreakpointState { } pub enum SessionState { - /// Represents a session that is building/initializing - /// even if a session doesn't have a pre build task this state - /// is used to run all the async tasks that are required to start the session - Booting(Option>>), + Building(Option>>), Running(RunningMode), } @@ -226,7 +223,7 @@ impl RunningMode { fn unset_breakpoints_from_paths(&self, paths: &Vec>, cx: &mut App) -> Task<()> { let tasks: Vec<_> = paths - .iter() + .into_iter() .map(|path| { self.request(dap_command::SetBreakpoints { source: client_source(path), @@ -431,7 +428,7 @@ impl RunningMode { let should_send_exception_breakpoints = capabilities .exception_breakpoint_filters .as_ref() - .is_some_and(|filters| !filters.is_empty()) + .map_or(false, |filters| !filters.is_empty()) || !configuration_done_supported; let supports_exception_filters = capabilities .supports_exception_filter_options @@ -508,12 +505,13 @@ impl RunningMode { .ok(); } - if configuration_done_supported { + let ret = if configuration_done_supported { this.request(ConfigurationDone {}) } else { Task::ready(Ok(())) } - .await + .await; + ret } }); @@ -576,7 +574,7 @@ impl SessionState { { match self { SessionState::Running(debug_adapter_client) => debug_adapter_client.request(request), - SessionState::Booting(_) => Task::ready(Err(anyhow!( + SessionState::Building(_) => Task::ready(Err(anyhow!( "no adapter running to send request: {request:?}" ))), } @@ -585,7 +583,7 @@ impl SessionState { /// Did this debug session stop at least once? pub(crate) fn has_ever_stopped(&self) -> bool { match self { - SessionState::Booting(_) => false, + SessionState::Building(_) => false, SessionState::Running(running_mode) => running_mode.has_ever_stopped, } } @@ -709,7 +707,9 @@ where T: LocalDapCommand + PartialEq + Eq + Hash, { fn dyn_eq(&self, rhs: &dyn CacheableCommand) -> bool { - (rhs as &dyn Any).downcast_ref::() == Some(self) + (rhs as &dyn Any) + .downcast_ref::() + .map_or(false, |rhs| self == rhs) } fn dyn_hash(&self, mut hasher: &mut dyn Hasher) { @@ -838,8 +838,8 @@ impl Session { }) .detach(); - Self { - mode: SessionState::Booting(None), + let this = Self { + mode: SessionState::Building(None), id: session_id, child_session_ids: HashSet::default(), parent_session, @@ -867,7 +867,9 @@ impl Session { task_context, memory: memory::Memory::new(), quirks, - } + }; + + this }) } @@ -877,7 +879,7 @@ impl Session { pub fn worktree(&self) -> Option> { match &self.mode { - SessionState::Booting(_) => None, + SessionState::Building(_) => None, SessionState::Running(local_mode) => local_mode.worktree.upgrade(), } } @@ -938,12 +940,14 @@ impl Session { .await?; this.update(cx, |this, cx| { match &mut this.mode { - SessionState::Booting(task) if task.is_some() => { + SessionState::Building(task) if task.is_some() => { task.take().unwrap().detach_and_log_err(cx); } - SessionState::Booting(_) => {} - SessionState::Running(_) => { - debug_panic!("Attempting to boot a session that is already running"); + _ => { + debug_assert!( + this.parent_session.is_some(), + "Booting a root debug session without a boot task" + ); } }; this.mode = SessionState::Running(mode); @@ -1039,7 +1043,7 @@ impl Session { pub fn binary(&self) -> Option<&DebugAdapterBinary> { match &self.mode { - SessionState::Booting(_) => None, + SessionState::Building(_) => None, SessionState::Running(running_mode) => Some(&running_mode.binary), } } @@ -1080,31 +1084,31 @@ impl Session { }) .detach(); - tx + return tx; } pub fn is_started(&self) -> bool { match &self.mode { - SessionState::Booting(_) => false, + SessionState::Building(_) => false, SessionState::Running(running) => running.is_started, } } pub fn is_building(&self) -> bool { - matches!(self.mode, SessionState::Booting(_)) + matches!(self.mode, SessionState::Building(_)) } pub fn as_running_mut(&mut self) -> Option<&mut RunningMode> { match &mut self.mode { SessionState::Running(local_mode) => Some(local_mode), - SessionState::Booting(_) => None, + SessionState::Building(_) => None, } } pub fn as_running(&self) -> Option<&RunningMode> { match &self.mode { SessionState::Running(local_mode) => Some(local_mode), - SessionState::Booting(_) => None, + SessionState::Building(_) => None, } } @@ -1298,7 +1302,7 @@ impl Session { SessionState::Running(local_mode) => { local_mode.initialize_sequence(&self.capabilities, initialize_rx, dap_store, cx) } - SessionState::Booting(_) => { + SessionState::Building(_) => { Task::ready(Err(anyhow!("cannot initialize, still building"))) } } @@ -1335,7 +1339,7 @@ impl Session { }) .detach(); } - SessionState::Booting(_) => {} + SessionState::Building(_) => {} } } @@ -1394,7 +1398,7 @@ impl Session { let breakpoint_store = self.breakpoint_store.clone(); if let Some((local, path)) = self.as_running_mut().and_then(|local| { let breakpoint = local.tmp_breakpoint.take()?; - let path = breakpoint.path; + let path = breakpoint.path.clone(); Some((local, path)) }) { local @@ -1625,7 +1629,7 @@ impl Session { + 'static, cx: &mut Context, ) -> Task> { - if !T::is_supported(capabilities) { + if !T::is_supported(&capabilities) { log::warn!( "Attempted to send a DAP request that isn't supported: {:?}", request @@ -1683,7 +1687,7 @@ impl Session { self.requests .entry((&*key.0 as &dyn Any).type_id()) .and_modify(|request_map| { - request_map.remove(key); + request_map.remove(&key); }); } @@ -1710,7 +1714,7 @@ impl Session { this.threads = result .into_iter() - .map(|thread| (ThreadId(thread.id), Thread::from(thread))) + .map(|thread| (ThreadId(thread.id), Thread::from(thread.clone()))) .collect(); this.invalidate_command_type::(); @@ -2141,7 +2145,7 @@ impl Session { ) } } - SessionState::Booting(build_task) => { + SessionState::Building(build_task) => { build_task.take(); Task::ready(Some(())) } @@ -2195,7 +2199,7 @@ impl Session { pub fn adapter_client(&self) -> Option> { match self.mode { SessionState::Running(ref local) => Some(local.client.clone()), - SessionState::Booting(_) => None, + SessionState::Building(_) => None, } } @@ -2553,7 +2557,10 @@ impl Session { mode: Option, cx: &mut Context, ) -> Task> { - let command = DataBreakpointInfoCommand { context, mode }; + let command = DataBreakpointInfoCommand { + context: context.clone(), + mode, + }; self.request(command, |_, response, _| response.ok(), cx) } diff --git a/crates/project/src/environment.rs b/crates/project/src/environment.rs index d109e307a8..7379a7ef72 100644 --- a/crates/project/src/environment.rs +++ b/crates/project/src/environment.rs @@ -198,7 +198,7 @@ async fn load_directory_shell_environment( ); }; - load_shell_environment(dir, load_direnv).await + load_shell_environment(&dir, load_direnv).await } Err(err) => ( None, diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 5cf298a8bf..28dd0e91e3 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -246,8 +246,6 @@ pub struct RepositorySnapshot { pub head_commit: Option, pub scan_id: u64, pub merge: MergeDetails, - pub remote_origin_url: Option, - pub remote_upstream_url: Option, } type JobId = u64; @@ -414,7 +412,6 @@ impl GitStore { pub fn init(client: &AnyProtoClient) { client.add_entity_request_handler(Self::handle_get_remotes); client.add_entity_request_handler(Self::handle_get_branches); - client.add_entity_request_handler(Self::handle_get_default_branch); client.add_entity_request_handler(Self::handle_change_branch); client.add_entity_request_handler(Self::handle_create_branch); client.add_entity_request_handler(Self::handle_git_init); @@ -442,7 +439,6 @@ impl GitStore { client.add_entity_request_handler(Self::handle_blame_buffer); client.add_entity_message_handler(Self::handle_update_repository); client.add_entity_message_handler(Self::handle_remove_repository); - client.add_entity_request_handler(Self::handle_git_clone); } pub fn is_local(&self) -> bool { @@ -561,7 +557,7 @@ impl GitStore { pub fn active_repository(&self) -> Option> { self.active_repo_id .as_ref() - .map(|id| self.repositories[id].clone()) + .map(|id| self.repositories[&id].clone()) } pub fn open_unstaged_diff( @@ -570,22 +566,23 @@ impl GitStore { cx: &mut Context, ) -> Task>> { let buffer_id = buffer.read(cx).remote_id(); - if let Some(diff_state) = self.diffs.get(&buffer_id) - && let Some(unstaged_diff) = diff_state + if let Some(diff_state) = self.diffs.get(&buffer_id) { + if let Some(unstaged_diff) = diff_state .read(cx) .unstaged_diff .as_ref() .and_then(|weak| weak.upgrade()) - { - if let Some(task) = - diff_state.update(cx, |diff_state, _| diff_state.wait_for_recalculation()) { - return cx.background_executor().spawn(async move { - task.await; - Ok(unstaged_diff) - }); + if let Some(task) = + diff_state.update(cx, |diff_state, _| diff_state.wait_for_recalculation()) + { + return cx.background_executor().spawn(async move { + task.await; + Ok(unstaged_diff) + }); + } + return Task::ready(Ok(unstaged_diff)); } - return Task::ready(Ok(unstaged_diff)); } let Some((repo, repo_path)) = @@ -626,22 +623,23 @@ impl GitStore { ) -> Task>> { let buffer_id = buffer.read(cx).remote_id(); - if let Some(diff_state) = self.diffs.get(&buffer_id) - && let Some(uncommitted_diff) = diff_state + if let Some(diff_state) = self.diffs.get(&buffer_id) { + if let Some(uncommitted_diff) = diff_state .read(cx) .uncommitted_diff .as_ref() .and_then(|weak| weak.upgrade()) - { - if let Some(task) = - diff_state.update(cx, |diff_state, _| diff_state.wait_for_recalculation()) { - return cx.background_executor().spawn(async move { - task.await; - Ok(uncommitted_diff) - }); + if let Some(task) = + diff_state.update(cx, |diff_state, _| diff_state.wait_for_recalculation()) + { + return cx.background_executor().spawn(async move { + task.await; + Ok(uncommitted_diff) + }); + } + return Task::ready(Ok(uncommitted_diff)); } - return Task::ready(Ok(uncommitted_diff)); } let Some((repo, repo_path)) = @@ -762,26 +760,29 @@ impl GitStore { log::debug!("open conflict set"); let buffer_id = buffer.read(cx).remote_id(); - if let Some(git_state) = self.diffs.get(&buffer_id) - && let Some(conflict_set) = git_state + if let Some(git_state) = self.diffs.get(&buffer_id) { + if let Some(conflict_set) = git_state .read(cx) .conflict_set .as_ref() .and_then(|weak| weak.upgrade()) - { - let conflict_set = conflict_set; - let buffer_snapshot = buffer.read(cx).text_snapshot(); + { + let conflict_set = conflict_set.clone(); + let buffer_snapshot = buffer.read(cx).text_snapshot(); - git_state.update(cx, |state, cx| { - let _ = state.reparse_conflict_markers(buffer_snapshot, cx); - }); + git_state.update(cx, |state, cx| { + let _ = state.reparse_conflict_markers(buffer_snapshot, cx); + }); - return conflict_set; + return conflict_set; + } } let is_unmerged = self .repository_and_path_for_buffer_id(buffer_id, cx) - .is_some_and(|(repo, path)| repo.read(cx).snapshot.has_conflict(&path)); + .map_or(false, |(repo, path)| { + repo.read(cx).snapshot.has_conflict(&path) + }); let git_store = cx.weak_entity(); let buffer_git_state = self .diffs @@ -912,7 +913,7 @@ impl GitStore { return Task::ready(Err(anyhow!("failed to find a git repository for buffer"))); }; let content = match &version { - Some(version) => buffer.rope_for_version(version), + Some(version) => buffer.rope_for_version(version).clone(), None => buffer.as_rope().clone(), }; let version = version.unwrap_or(buffer.version()); @@ -1146,26 +1147,29 @@ impl GitStore { for (buffer_id, diff) in self.diffs.iter() { if let Some((buffer_repo, repo_path)) = self.repository_and_path_for_buffer_id(*buffer_id, cx) - && buffer_repo == repo { - diff.update(cx, |diff, cx| { - if let Some(conflict_set) = &diff.conflict_set { - let conflict_status_changed = - conflict_set.update(cx, |conflict_set, cx| { - let has_conflict = repo_snapshot.has_conflict(&repo_path); - conflict_set.set_has_conflict(has_conflict, cx) - })?; - if conflict_status_changed { - let buffer_store = self.buffer_store.read(cx); - if let Some(buffer) = buffer_store.get(*buffer_id) { - let _ = diff - .reparse_conflict_markers(buffer.read(cx).text_snapshot(), cx); + if buffer_repo == repo { + diff.update(cx, |diff, cx| { + if let Some(conflict_set) = &diff.conflict_set { + let conflict_status_changed = + conflict_set.update(cx, |conflict_set, cx| { + let has_conflict = repo_snapshot.has_conflict(&repo_path); + conflict_set.set_has_conflict(has_conflict, cx) + })?; + if conflict_status_changed { + let buffer_store = self.buffer_store.read(cx); + if let Some(buffer) = buffer_store.get(*buffer_id) { + let _ = diff.reparse_conflict_markers( + buffer.read(cx).text_snapshot(), + cx, + ); + } } } - } - anyhow::Ok(()) - }) - .ok(); + anyhow::Ok(()) + }) + .ok(); + } } } cx.emit(GitStoreEvent::RepositoryUpdated( @@ -1269,7 +1273,7 @@ impl GitStore { ) { match event { BufferStoreEvent::BufferAdded(buffer) => { - cx.subscribe(buffer, |this, buffer, event, cx| { + cx.subscribe(&buffer, |this, buffer, event, cx| { if let BufferEvent::LanguageChanged = event { let buffer_id = buffer.read(cx).remote_id(); if let Some(diff_state) = this.diffs.get(&buffer_id) { @@ -1287,7 +1291,7 @@ impl GitStore { } } BufferStoreEvent::BufferDropped(buffer_id) => { - self.diffs.remove(buffer_id); + self.diffs.remove(&buffer_id); for diffs in self.shared_diffs.values_mut() { diffs.remove(buffer_id); } @@ -1376,8 +1380,8 @@ impl GitStore { repository.update(cx, |repository, cx| { let repo_abs_path = &repository.work_directory_abs_path; if changed_repos.iter().any(|update| { - update.old_work_directory_abs_path.as_ref() == Some(repo_abs_path) - || update.new_work_directory_abs_path.as_ref() == Some(repo_abs_path) + update.old_work_directory_abs_path.as_ref() == Some(&repo_abs_path) + || update.new_work_directory_abs_path.as_ref() == Some(&repo_abs_path) }) { repository.reload_buffer_diff_bases(cx); } @@ -1458,45 +1462,6 @@ impl GitStore { } } - pub fn git_clone( - &self, - repo: String, - path: impl Into>, - cx: &App, - ) -> Task> { - let path = path.into(); - match &self.state { - GitStoreState::Local { fs, .. } => { - let fs = fs.clone(); - cx.background_executor() - .spawn(async move { fs.git_clone(&repo, &path).await }) - } - GitStoreState::Ssh { - upstream_client, - upstream_project_id, - .. - } => { - let request = upstream_client.request(proto::GitClone { - project_id: upstream_project_id.0, - abs_path: path.to_string_lossy().to_string(), - remote_repo: repo, - }); - - cx.background_spawn(async move { - let result = request.await?; - - match result.success { - true => Ok(()), - false => Err(anyhow!("Git Clone failed")), - } - }) - } - GitStoreState::Remote { .. } => { - Task::ready(Err(anyhow!("Git Clone isn't supported for remote users"))) - } - } - } - async fn handle_update_repository( this: Entity, envelope: TypedEnvelope, @@ -1506,7 +1471,10 @@ impl GitStore { let mut update = envelope.payload; let id = RepositoryId::from_proto(update.id); - let client = this.upstream_client().context("no upstream client")?; + let client = this + .upstream_client() + .context("no upstream client")? + .clone(); let mut is_new = false; let repo = this.repositories.entry(id).or_insert_with(|| { @@ -1525,7 +1493,7 @@ impl GitStore { }); if is_new { this._subscriptions - .push(cx.subscribe(repo, Self::on_repository_event)) + .push(cx.subscribe(&repo, Self::on_repository_event)) } repo.update(cx, { @@ -1580,22 +1548,6 @@ impl GitStore { Ok(proto::Ack {}) } - async fn handle_git_clone( - this: Entity, - envelope: TypedEnvelope, - cx: AsyncApp, - ) -> Result { - let path: Arc = PathBuf::from(envelope.payload.abs_path).into(); - let repo_name = envelope.payload.remote_repo; - let result = cx - .update(|cx| this.read(cx).git_clone(repo_name, path, cx))? - .await; - - Ok(proto::GitCloneResponse { - success: result.is_ok(), - }) - } - async fn handle_fetch( this: Entity, envelope: TypedEnvelope, @@ -1884,23 +1836,6 @@ impl GitStore { .collect::>(), }) } - async fn handle_get_default_branch( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); - let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?; - - let branch = repository_handle - .update(&mut cx, |repository_handle, _| { - repository_handle.default_branch() - })? - .await?? - .map(Into::into); - - Ok(proto::GetDefaultBranchResponse { branch }) - } async fn handle_create_branch( this: Entity, envelope: TypedEnvelope, @@ -2220,13 +2155,13 @@ impl GitStore { ) -> Result<()> { let buffer_id = BufferId::new(request.payload.buffer_id)?; this.update(&mut cx, |this, cx| { - if let Some(diff_state) = this.diffs.get_mut(&buffer_id) - && let Some(buffer) = this.buffer_store.read(cx).get(buffer_id) - { - let buffer = buffer.read(cx).text_snapshot(); - diff_state.update(cx, |diff_state, cx| { - diff_state.handle_base_texts_updated(buffer, request.payload, cx); - }) + if let Some(diff_state) = this.diffs.get_mut(&buffer_id) { + if let Some(buffer) = this.buffer_store.read(cx).get(buffer_id) { + let buffer = buffer.read(cx).text_snapshot(); + diff_state.update(cx, |diff_state, cx| { + diff_state.handle_base_texts_updated(buffer, request.payload, cx); + }) + } } }) } @@ -2338,20 +2273,16 @@ impl GitStore { return None; }; - let mut paths = Vec::new(); + let mut paths = vec![]; // All paths prefixed by a given repo will constitute a continuous range. while let Some(path) = entries.get(ix) && let Some(repo_path) = - RepositorySnapshot::abs_path_to_repo_path_inner(&repo_path, path) + RepositorySnapshot::abs_path_to_repo_path_inner(&repo_path, &path) { paths.push((repo_path, ix)); ix += 1; } - if paths.is_empty() { - None - } else { - Some((repo, paths)) - } + Some((repo, paths)) }); tasks.push_back(task); } @@ -2496,14 +2427,14 @@ impl BufferGitState { pub fn wait_for_recalculation(&mut self) -> Option + use<>> { if *self.recalculating_tx.borrow() { let mut rx = self.recalculating_tx.subscribe(); - Some(async move { + return Some(async move { loop { let is_recalculating = rx.recv().await; if is_recalculating != Some(true) { break; } } - }) + }); } else { None } @@ -2742,8 +2673,6 @@ impl RepositorySnapshot { head_commit: None, scan_id: 0, merge: Default::default(), - remote_origin_url: None, - remote_upstream_url: None, } } @@ -2763,7 +2692,6 @@ impl RepositorySnapshot { .iter() .map(|repo_path| repo_path.to_proto()) .collect(), - merge_message: self.merge.message.as_ref().map(|msg| msg.to_string()), project_id, id: self.id.to_proto(), abs_path: self.work_directory_abs_path.to_proto(), @@ -2826,7 +2754,6 @@ impl RepositorySnapshot { .iter() .map(|path| path.as_ref().to_proto()) .collect(), - merge_message: self.merge.message.as_ref().map(|msg| msg.to_string()), project_id, id: self.id.to_proto(), abs_path: self.work_directory_abs_path.to_proto(), @@ -2866,15 +2793,15 @@ impl RepositorySnapshot { } pub fn had_conflict_on_last_merge_head_change(&self, repo_path: &RepoPath) -> bool { - self.merge.conflicted_paths.contains(repo_path) + self.merge.conflicted_paths.contains(&repo_path) } pub fn has_conflict(&self, repo_path: &RepoPath) -> bool { let had_conflict_on_last_merge_head_change = - self.merge.conflicted_paths.contains(repo_path); + self.merge.conflicted_paths.contains(&repo_path); let has_conflict_currently = self - .status_for_path(repo_path) - .is_some_and(|entry| entry.status.is_conflicted()); + .status_for_path(&repo_path) + .map_or(false, |entry| entry.status.is_conflicted()); had_conflict_on_last_merge_head_change || has_conflict_currently } @@ -3415,6 +3342,7 @@ impl Repository { reset_mode: ResetMode, _cx: &mut App, ) -> oneshot::Receiver> { + let commit = commit.to_string(); let id = self.id; self.send_job(None, move |git_repo, _| async move { @@ -3521,13 +3449,14 @@ impl Repository { let Some(project_path) = self.repo_path_to_project_path(path, cx) else { continue; }; - if let Some(buffer) = buffer_store.get_by_path(&project_path) - && buffer + if let Some(buffer) = buffer_store.get_by_path(&project_path) { + if buffer .read(cx) .file() - .is_some_and(|file| file.disk_state().exists()) - { - save_futures.push(buffer_store.save_buffer(buffer, cx)); + .map_or(false, |file| file.disk_state().exists()) + { + save_futures.push(buffer_store.save_buffer(buffer, cx)); + } } } }) @@ -3587,13 +3516,14 @@ impl Repository { let Some(project_path) = self.repo_path_to_project_path(path, cx) else { continue; }; - if let Some(buffer) = buffer_store.get_by_path(&project_path) - && buffer + if let Some(buffer) = buffer_store.get_by_path(&project_path) { + if buffer .read(cx) .file() - .is_some_and(|file| file.disk_state().exists()) - { - save_futures.push(buffer_store.save_buffer(buffer, cx)); + .map_or(false, |file| file.disk_state().exists()) + { + save_futures.push(buffer_store.save_buffer(buffer, cx)); + } } } }) @@ -3640,7 +3570,7 @@ impl Repository { let to_stage = self .cached_status() .filter(|entry| !entry.status.staging().is_fully_staged()) - .map(|entry| entry.repo_path) + .map(|entry| entry.repo_path.clone()) .collect(); self.stage_entries(to_stage, cx) } @@ -3649,13 +3579,16 @@ impl Repository { let to_unstage = self .cached_status() .filter(|entry| entry.status.staging().has_staged()) - .map(|entry| entry.repo_path) + .map(|entry| entry.repo_path.clone()) .collect(); self.unstage_entries(to_unstage, cx) } pub fn stash_all(&mut self, cx: &mut Context) -> Task> { - let to_stash = self.cached_status().map(|entry| entry.repo_path).collect(); + let to_stash = self + .cached_status() + .map(|entry| entry.repo_path.clone()) + .collect(); self.stash_entries(to_stash, cx) } @@ -4092,25 +4025,6 @@ impl Repository { }) } - pub fn default_branch(&mut self) -> oneshot::Receiver>> { - let id = self.id; - self.send_job(None, move |repo, _| async move { - match repo { - RepositoryState::Local { backend, .. } => backend.default_branch().await, - RepositoryState::Remote { project_id, client } => { - let response = client - .request(proto::GetDefaultBranch { - project_id: project_id.0, - repository_id: id.to_proto(), - }) - .await?; - - anyhow::Ok(response.branch.map(SharedString::from)) - } - } - }) - } - pub fn diff(&mut self, diff_type: DiffType, _cx: &App) -> oneshot::Receiver> { let id = self.id; self.send_job(None, move |repo, _cx| async move { @@ -4251,7 +4165,6 @@ impl Repository { .map(proto_to_commit_details); self.snapshot.merge.conflicted_paths = conflicted_paths; - self.snapshot.merge.message = update.merge_message.map(SharedString::from); let edits = update .removed_statuses @@ -4328,8 +4241,7 @@ impl Repository { bail!("not a local repository") }; let (snapshot, events) = this - .update(&mut cx, |this, _| { - this.paths_needing_status_update.clear(); + .read_with(&mut cx, |this, _| { compute_snapshot( this.id, this.work_directory_abs_path.clone(), @@ -4404,13 +4316,14 @@ impl Repository { } if let Some(job) = jobs.pop_front() { - if let Some(current_key) = &job.key - && jobs + if let Some(current_key) = &job.key { + if jobs .iter() .any(|other_job| other_job.key.as_ref() == Some(current_key)) { continue; } + } (job.job)(state.clone(), cx).await; } else if let Some(job) = job_rx.next().await { jobs.push_back(job); @@ -4441,12 +4354,13 @@ impl Repository { } if let Some(job) = jobs.pop_front() { - if let Some(current_key) = &job.key - && jobs + if let Some(current_key) = &job.key { + if jobs .iter() .any(|other_job| other_job.key.as_ref() == Some(current_key)) - { - continue; + { + continue; + } } (job.job)(state.clone(), cx).await; } else if let Some(job) = job_rx.next().await { @@ -4557,9 +4471,6 @@ impl Repository { }; let paths = changed_paths.iter().cloned().collect::>(); - if paths.is_empty() { - return Ok(()); - } let statuses = backend.status(&paths).await?; let changed_path_statuses = cx @@ -4570,10 +4481,10 @@ impl Repository { for (repo_path, status) in &*statuses.entries { changed_paths.remove(repo_path); - if cursor.seek_forward(&PathTarget::Path(repo_path), Bias::Left) - && cursor.item().is_some_and(|entry| entry.status == *status) - { - continue; + if cursor.seek_forward(&PathTarget::Path(repo_path), Bias::Left) { + if cursor.item().is_some_and(|entry| entry.status == *status) { + continue; + } } changed_path_statuses.push(Edit::Insert(StatusEntry { @@ -4888,10 +4799,6 @@ async fn compute_snapshot( None => None, }; - // Used by edit prediction data collection - let remote_origin_url = backend.remote_url("origin"); - let remote_upstream_url = backend.remote_url("upstream"); - let snapshot = RepositorySnapshot { id, statuses_by_path, @@ -4900,8 +4807,6 @@ async fn compute_snapshot( branch, head_commit, merge: merge_details, - remote_origin_url, - remote_upstream_url, }; Ok((snapshot, events)) diff --git a/crates/project/src/git_store/conflict_set.rs b/crates/project/src/git_store/conflict_set.rs index 313a1e90ad..27b191f65f 100644 --- a/crates/project/src/git_store/conflict_set.rs +++ b/crates/project/src/git_store/conflict_set.rs @@ -369,7 +369,7 @@ mod tests { .unindent(); let buffer_id = BufferId::new(1).unwrap(); - let buffer = Buffer::new(0, buffer_id, test_content); + let buffer = Buffer::new(0, buffer_id, test_content.to_string()); let snapshot = buffer.snapshot(); let conflict_snapshot = ConflictSet::parse(&snapshot); @@ -400,7 +400,7 @@ mod tests { >>>>>>> "# .unindent(); let buffer_id = BufferId::new(1).unwrap(); - let buffer = Buffer::new(0, buffer_id, test_content); + let buffer = Buffer::new(0, buffer_id, test_content.to_string()); let snapshot = buffer.snapshot(); let conflict_snapshot = ConflictSet::parse(&snapshot); @@ -653,7 +653,7 @@ mod tests { cx.run_until_parked(); conflict_set.update(cx, |conflict_set, _| { - assert!(!conflict_set.has_conflict); + assert_eq!(conflict_set.has_conflict, false); assert_eq!(conflict_set.snapshot.conflicts.len(), 0); }); diff --git a/crates/project/src/git_store/git_traversal.rs b/crates/project/src/git_store/git_traversal.rs index eee492e482..777042cb02 100644 --- a/crates/project/src/git_store/git_traversal.rs +++ b/crates/project/src/git_store/git_traversal.rs @@ -42,8 +42,8 @@ impl<'a> GitTraversal<'a> { // other_repo/ // .git/ // our_query.txt - let query = path.ancestors(); - for query in query { + let mut query = path.ancestors(); + while let Some(query) = query.next() { let (_, snapshot) = self .repo_root_to_snapshot .range(Path::new("")..=query) @@ -110,7 +110,11 @@ impl<'a> GitTraversal<'a> { } pub fn advance(&mut self) -> bool { - let found = self.traversal.advance_by(1); + self.advance_by(1) + } + + pub fn advance_by(&mut self, count: usize) -> bool { + let found = self.traversal.advance_by(count); self.synchronize_statuses(false); found } @@ -182,11 +186,11 @@ impl<'a> Iterator for ChildEntriesGitIter<'a> { type Item = GitEntryRef<'a>; fn next(&mut self) -> Option { - if let Some(item) = self.traversal.entry() - && item.path.starts_with(self.parent_path) - { - self.traversal.advance_to_sibling(); - return Some(item); + if let Some(item) = self.traversal.entry() { + if item.path.starts_with(self.parent_path) { + self.traversal.advance_to_sibling(); + return Some(item); + } } None } @@ -199,7 +203,7 @@ pub struct GitEntryRef<'a> { } impl GitEntryRef<'_> { - pub fn to_owned(self) -> GitEntry { + pub fn to_owned(&self) -> GitEntry { GitEntry { entry: self.entry.clone(), git_summary: self.git_summary, @@ -211,7 +215,7 @@ impl Deref for GitEntryRef<'_> { type Target = Entry; fn deref(&self) -> &Self::Target { - self.entry + &self.entry } } diff --git a/crates/project/src/image_store.rs b/crates/project/src/image_store.rs index e499d4e026..79f134b91a 100644 --- a/crates/project/src/image_store.rs +++ b/crates/project/src/image_store.rs @@ -224,7 +224,7 @@ impl ProjectItem for ImageItem { path: &ProjectPath, cx: &mut App, ) -> Option>>> { - if is_image_file(project, path, cx) { + if is_image_file(&project, &path, cx) { Some(cx.spawn({ let path = path.clone(); let project = project.clone(); @@ -244,7 +244,7 @@ impl ProjectItem for ImageItem { } fn project_path(&self, cx: &App) -> Option { - Some(self.project_path(cx)) + Some(self.project_path(cx).clone()) } fn is_dirty(&self) -> bool { @@ -375,6 +375,7 @@ impl ImageStore { let (mut tx, rx) = postage::watch::channel(); entry.insert(rx.clone()); + let project_path = project_path.clone(); let load_image = self .state .open_image(project_path.path.clone(), worktree, cx); @@ -445,12 +446,15 @@ impl ImageStore { event: &ImageItemEvent, cx: &mut Context, ) { - if let ImageItemEvent::FileHandleChanged = event - && let Some(local) = self.state.as_local() - { - local.update(cx, |local, cx| { - local.image_changed_file(image, cx); - }) + match event { + ImageItemEvent::FileHandleChanged => { + if let Some(local) = self.state.as_local() { + local.update(cx, |local, cx| { + local.image_changed_file(image, cx); + }) + } + } + _ => {} } } } @@ -527,10 +531,13 @@ impl ImageStoreImpl for Entity { impl LocalImageStore { fn subscribe_to_worktree(&mut self, worktree: &Entity, cx: &mut Context) { cx.subscribe(worktree, |this, worktree, event, cx| { - if worktree.read(cx).is_local() - && let worktree::Event::UpdatedEntries(changes) = event - { - this.local_worktree_entries_changed(&worktree, changes, cx); + if worktree.read(cx).is_local() { + match event { + worktree::Event::UpdatedEntries(changes) => { + this.local_worktree_entries_changed(&worktree, changes, cx); + } + _ => {} + } } }) .detach(); diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index ce7a871d1a..958921a0e6 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -332,9 +332,9 @@ impl LspCommand for PrepareRename { _: Entity, buffer: Entity, _: LanguageServerId, - cx: AsyncApp, + mut cx: AsyncApp, ) -> Result { - buffer.read_with(&cx, |buffer, _| match message { + buffer.read_with(&mut cx, |buffer, _| match message { Some(lsp::PrepareRenameResponse::Range(range)) | Some(lsp::PrepareRenameResponse::RangeWithPlaceholder { range, .. }) => { let Range { start, end } = range_from_lsp(range); @@ -386,7 +386,7 @@ impl LspCommand for PrepareRename { .await?; Ok(Self { - position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, + position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, }) } @@ -500,12 +500,13 @@ impl LspCommand for PerformRename { mut cx: AsyncApp, ) -> Result { if let Some(edit) = message { - let (_, lsp_server) = + let (lsp_adapter, lsp_server) = language_server_for_buffer(&lsp_store, &buffer, server_id, &mut cx)?; LocalLspStore::deserialize_workspace_edit( lsp_store, edit, self.push_to_history, + lsp_adapter, lsp_server, &mut cx, ) @@ -543,7 +544,7 @@ impl LspCommand for PerformRename { })? .await?; Ok(Self { - position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, + position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, new_name: message.new_name, push_to_history: false, }) @@ -658,7 +659,7 @@ impl LspCommand for GetDefinitions { })? .await?; Ok(Self { - position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, + position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, }) } @@ -761,7 +762,7 @@ impl LspCommand for GetDeclarations { })? .await?; Ok(Self { - position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, + position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, }) } @@ -863,7 +864,7 @@ impl LspCommand for GetImplementations { })? .await?; Ok(Self { - position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, + position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, }) } @@ -962,7 +963,7 @@ impl LspCommand for GetTypeDefinitions { })? .await?; Ok(Self { - position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, + position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, }) } @@ -1115,12 +1116,18 @@ pub async fn location_links_from_lsp( } } - let (_, language_server) = language_server_for_buffer(&lsp_store, &buffer, server_id, &mut cx)?; + let (lsp_adapter, language_server) = + language_server_for_buffer(&lsp_store, &buffer, server_id, &mut cx)?; let mut definitions = Vec::new(); for (origin_range, target_uri, target_range) in unresolved_links { let target_buffer_handle = lsp_store .update(&mut cx, |this, cx| { - this.open_local_buffer_via_lsp(target_uri, language_server.server_id(), cx) + this.open_local_buffer_via_lsp( + target_uri, + language_server.server_id(), + lsp_adapter.name.clone(), + cx, + ) })? .await?; @@ -1165,7 +1172,8 @@ pub async fn location_link_from_lsp( server_id: LanguageServerId, cx: &mut AsyncApp, ) -> Result { - let (_, language_server) = language_server_for_buffer(lsp_store, buffer, server_id, cx)?; + let (lsp_adapter, language_server) = + language_server_for_buffer(&lsp_store, &buffer, server_id, cx)?; let (origin_range, target_uri, target_range) = ( link.origin_selection_range, @@ -1175,7 +1183,12 @@ pub async fn location_link_from_lsp( let target_buffer_handle = lsp_store .update(cx, |lsp_store, cx| { - lsp_store.open_local_buffer_via_lsp(target_uri, language_server.server_id(), cx) + lsp_store.open_local_buffer_via_lsp( + target_uri, + language_server.server_id(), + lsp_adapter.name.clone(), + cx, + ) })? .await?; @@ -1313,7 +1326,7 @@ impl LspCommand for GetReferences { mut cx: AsyncApp, ) -> Result> { let mut references = Vec::new(); - let (_, language_server) = + let (lsp_adapter, language_server) = language_server_for_buffer(&lsp_store, &buffer, server_id, &mut cx)?; if let Some(locations) = locations { @@ -1323,6 +1336,7 @@ impl LspCommand for GetReferences { lsp_store.open_local_buffer_via_lsp( lsp_location.uri, language_server.server_id(), + lsp_adapter.name.clone(), cx, ) })? @@ -1330,7 +1344,7 @@ impl LspCommand for GetReferences { target_buffer_handle .clone() - .read_with(&cx, |target_buffer, _| { + .read_with(&mut cx, |target_buffer, _| { let target_start = target_buffer .clip_point_utf16(point_from_lsp(lsp_location.range.start), Bias::Left); let target_end = target_buffer @@ -1374,7 +1388,7 @@ impl LspCommand for GetReferences { })? .await?; Ok(Self { - position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, + position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, }) } @@ -1484,9 +1498,9 @@ impl LspCommand for GetDocumentHighlights { _: Entity, buffer: Entity, _: LanguageServerId, - cx: AsyncApp, + mut cx: AsyncApp, ) -> Result> { - buffer.read_with(&cx, |buffer, _| { + buffer.read_with(&mut cx, |buffer, _| { let mut lsp_highlights = lsp_highlights.unwrap_or_default(); lsp_highlights.sort_unstable_by_key(|h| (h.range.start, Reverse(h.range.end))); lsp_highlights @@ -1534,7 +1548,7 @@ impl LspCommand for GetDocumentHighlights { })? .await?; Ok(Self { - position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, + position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, }) } @@ -1865,7 +1879,7 @@ impl LspCommand for GetSignatureHelp { })? .await .with_context(|| format!("waiting for version for buffer {}", buffer.entity_id()))?; - let buffer_snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot())?; + let buffer_snapshot = buffer.read_with(&mut cx, |buffer, _| buffer.snapshot())?; Ok(Self { position: payload .position @@ -1947,13 +1961,13 @@ impl LspCommand for GetHover { _: Entity, buffer: Entity, _: LanguageServerId, - cx: AsyncApp, + mut cx: AsyncApp, ) -> Result { let Some(hover) = message else { return Ok(None); }; - let (language, range) = buffer.read_with(&cx, |buffer, _| { + let (language, range) = buffer.read_with(&mut cx, |buffer, _| { ( buffer.language().cloned(), hover.range.map(|range| { @@ -2039,7 +2053,7 @@ impl LspCommand for GetHover { })? .await?; Ok(Self { - position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, + position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, }) } @@ -2113,7 +2127,7 @@ impl LspCommand for GetHover { return Ok(None); } - let language = buffer.read_with(&cx, |buffer, _| buffer.language().cloned())?; + let language = buffer.read_with(&mut cx, |buffer, _| buffer.language().cloned())?; let range = if let (Some(start), Some(end)) = (message.start, message.end) { language::proto::deserialize_anchor(start) .and_then(|start| language::proto::deserialize_anchor(end).map(|end| start..end)) @@ -2140,16 +2154,6 @@ impl LspCommand for GetHover { } } -impl GetCompletions { - pub fn can_resolve_completions(capabilities: &lsp::ServerCapabilities) -> bool { - capabilities - .completion_provider - .as_ref() - .and_then(|options| options.resolve_provider) - .unwrap_or(false) - } -} - #[async_trait(?Send)] impl LspCommand for GetCompletions { type Response = CoreCompletionResponse; @@ -2208,7 +2212,7 @@ impl LspCommand for GetCompletions { let unfiltered_completions_count = completions.len(); let language_server_adapter = lsp_store - .read_with(&cx, |lsp_store, _| { + .read_with(&mut cx, |lsp_store, _| { lsp_store.language_server_adapter_for_id(server_id) })? .with_context(|| format!("no language server with id {server_id}"))?; @@ -2341,14 +2345,15 @@ impl LspCommand for GetCompletions { .zip(completion_edits) .map(|(mut lsp_completion, mut edit)| { LineEnding::normalize(&mut edit.new_text); - if lsp_completion.data.is_none() - && let Some(default_data) = lsp_defaults + if lsp_completion.data.is_none() { + if let Some(default_data) = lsp_defaults .as_ref() .and_then(|item_defaults| item_defaults.data.clone()) - { - // Servers (e.g. JDTLS) prefer unchanged completions, when resolving the items later, - // so we do not insert the defaults here, but `data` is needed for resolving, so this is an exception. - lsp_completion.data = Some(default_data); + { + // Servers (e.g. JDTLS) prefer unchanged completions, when resolving the items later, + // so we do not insert the defaults here, but `data` is needed for resolving, so this is an exception. + lsp_completion.data = Some(default_data); + } } CoreCompletion { replace_range: edit.replace_range, @@ -2394,7 +2399,7 @@ impl LspCommand for GetCompletions { .position .and_then(language::proto::deserialize_anchor) .map(|p| { - buffer.read_with(&cx, |buffer, _| { + buffer.read_with(&mut cx, |buffer, _| { buffer.clip_point_utf16(Unclipped(p.to_point_utf16(buffer)), Bias::Left) }) }) @@ -2501,8 +2506,8 @@ pub(crate) fn parse_completion_text_edit( }; Some(ParsedCompletionEdit { - insert_range, - replace_range, + insert_range: insert_range, + replace_range: replace_range, new_text: new_text.clone(), }) } @@ -2595,9 +2600,11 @@ impl LspCommand for GetCodeActions { server_id: LanguageServerId, cx: AsyncApp, ) -> Result> { - let requested_kinds_set = self - .kinds - .map(|kinds| kinds.into_iter().collect::>()); + let requested_kinds_set = if let Some(kinds) = self.kinds { + Some(kinds.into_iter().collect::>()) + } else { + None + }; let language_server = cx.update(|cx| { lsp_store @@ -2620,10 +2627,10 @@ impl LspCommand for GetCodeActions { .filter_map(|entry| { let (lsp_action, resolved) = match entry { lsp::CodeActionOrCommand::CodeAction(lsp_action) => { - if let Some(command) = lsp_action.command.as_ref() - && !available_commands.contains(&command.command) - { - return None; + if let Some(command) = lsp_action.command.as_ref() { + if !available_commands.contains(&command.command) { + return None; + } } (LspAction::Action(Box::new(lsp_action)), false) } @@ -2638,9 +2645,10 @@ impl LspCommand for GetCodeActions { if let Some((requested_kinds, kind)) = requested_kinds_set.as_ref().zip(lsp_action.action_kind()) - && !requested_kinds.contains(&kind) { - return None; + if !requested_kinds.contains(&kind) { + return None; + } } Some(CodeAction { @@ -2737,7 +2745,7 @@ impl GetCodeActions { Some(lsp::CodeActionProviderCapability::Options(CodeActionOptions { code_action_kinds: Some(supported_action_kinds), .. - })) => Some(supported_action_kinds), + })) => Some(supported_action_kinds.clone()), _ => capabilities.code_action_kinds, } } @@ -2754,23 +2762,6 @@ impl GetCodeActions { } } -impl OnTypeFormatting { - pub fn supports_on_type_formatting(trigger: &str, capabilities: &ServerCapabilities) -> bool { - let Some(on_type_formatting_options) = &capabilities.document_on_type_formatting_provider - else { - return false; - }; - on_type_formatting_options - .first_trigger_character - .contains(trigger) - || on_type_formatting_options - .more_trigger_character - .iter() - .flatten() - .any(|chars| chars.contains(trigger)) - } -} - #[async_trait(?Send)] impl LspCommand for OnTypeFormatting { type Response = Option; @@ -2782,7 +2773,20 @@ impl LspCommand for OnTypeFormatting { } fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - Self::supports_on_type_formatting(&self.trigger, &capabilities.server_capabilities) + let Some(on_type_formatting_options) = &capabilities + .server_capabilities + .document_on_type_formatting_provider + else { + return false; + }; + on_type_formatting_options + .first_trigger_character + .contains(&self.trigger) + || on_type_formatting_options + .more_trigger_character + .iter() + .flatten() + .any(|chars| chars.contains(&self.trigger)) } fn to_lsp( @@ -2860,7 +2864,7 @@ impl LspCommand for OnTypeFormatting { })?; Ok(Self { - position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, + position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, trigger: message.trigger.clone(), options, push_to_history: false, @@ -3266,16 +3270,6 @@ impl InlayHints { }) .unwrap_or(false) } - - pub fn check_capabilities(capabilities: &ServerCapabilities) -> bool { - capabilities - .inlay_hint_provider - .as_ref() - .is_some_and(|inlay_hint_provider| match inlay_hint_provider { - lsp::OneOf::Left(enabled) => *enabled, - lsp::OneOf::Right(_) => true, - }) - } } #[async_trait(?Send)] @@ -3289,7 +3283,17 @@ impl LspCommand for InlayHints { } fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - Self::check_capabilities(&capabilities.server_capabilities) + let Some(inlay_hint_provider) = &capabilities.server_capabilities.inlay_hint_provider + else { + return false; + }; + match inlay_hint_provider { + lsp::OneOf::Left(enabled) => *enabled, + lsp::OneOf::Right(inlay_hint_capabilities) => match inlay_hint_capabilities { + lsp::InlayHintServerCapabilities::Options(_) => true, + lsp::InlayHintServerCapabilities::RegistrationOptions(_) => false, + }, + } } fn to_lsp( @@ -3444,7 +3448,10 @@ impl LspCommand for GetCodeLens { capabilities .server_capabilities .code_lens_provider - .is_some() + .as_ref() + .map_or(false, |code_lens_options| { + code_lens_options.resolve_provider.unwrap_or(false) + }) } fn to_lsp( @@ -3469,9 +3476,9 @@ impl LspCommand for GetCodeLens { lsp_store: Entity, buffer: Entity, server_id: LanguageServerId, - cx: AsyncApp, + mut cx: AsyncApp, ) -> anyhow::Result> { - let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot())?; + let snapshot = buffer.read_with(&mut cx, |buffer, _| buffer.snapshot())?; let language_server = cx.update(|cx| { lsp_store .read(cx) @@ -3573,18 +3580,6 @@ impl LspCommand for GetCodeLens { } } -impl LinkedEditingRange { - pub fn check_server_capabilities(capabilities: ServerCapabilities) -> bool { - let Some(linked_editing_options) = capabilities.linked_editing_range_provider else { - return false; - }; - if let LinkedEditingRangeServerCapabilities::Simple(false) = linked_editing_options { - return false; - } - true - } -} - #[async_trait(?Send)] impl LspCommand for LinkedEditingRange { type Response = Vec>; @@ -3596,7 +3591,16 @@ impl LspCommand for LinkedEditingRange { } fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - Self::check_server_capabilities(capabilities.server_capabilities) + let Some(linked_editing_options) = &capabilities + .server_capabilities + .linked_editing_range_provider + else { + return false; + }; + if let LinkedEditingRangeServerCapabilities::Simple(false) = linked_editing_options { + return false; + } + true } fn to_lsp( @@ -3790,7 +3794,7 @@ impl GetDocumentDiagnostics { }, uri: lsp::Url::parse(&info.location_url.unwrap()).unwrap(), }, - message: info.message, + message: info.message.clone(), } }) .collect::>(); @@ -3818,11 +3822,12 @@ impl GetDocumentDiagnostics { _ => None, }, code, - code_description: diagnostic - .code_description - .map(|code_description| CodeDescription { + code_description: match diagnostic.code_description { + Some(code_description) => Some(CodeDescription { href: Some(lsp::Url::parse(&code_description).unwrap()), }), + None => None, + }, related_information: Some(related_information), tags: Some(tags), source: diagnostic.source.clone(), @@ -4213,9 +4218,8 @@ impl LspCommand for GetDocumentColor { server_capabilities .server_capabilities .color_provider - .as_ref() .is_some_and(|capability| match capability { - lsp::ColorProviderCapability::Simple(supported) => *supported, + lsp::ColorProviderCapability::Simple(supported) => supported, lsp::ColorProviderCapability::ColorProvider(..) => true, lsp::ColorProviderCapability::Options(..) => true, }) @@ -4487,8 +4491,9 @@ mod tests { data: Some(json!({"detail": "test detail"})), }; - let proto_diagnostic = GetDocumentDiagnostics::serialize_lsp_diagnostic(lsp_diagnostic) - .expect("Failed to serialize diagnostic"); + let proto_diagnostic = + GetDocumentDiagnostics::serialize_lsp_diagnostic(lsp_diagnostic.clone()) + .expect("Failed to serialize diagnostic"); let start = proto_diagnostic.start.unwrap(); let end = proto_diagnostic.end.unwrap(); diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index deebaedd74..af3df72c29 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -1,14 +1,3 @@ -//! LSP store provides unified access to the language server protocol. -//! The consumers of LSP store can interact with language servers without knowing exactly which language server they're interacting with. -//! -//! # Local/Remote LSP Stores -//! This module is split up into three distinct parts: -//! - [`LocalLspStore`], which is ran on the host machine (either project host or SSH host), that manages the lifecycle of language servers. -//! - [`RemoteLspStore`], which is ran on the remote machine (project guests) which is mostly about passing through the requests via RPC. -//! The remote stores don't really care about which language server they're running against - they don't usually get to decide which language server is going to responsible for handling their request. -//! - [`LspStore`], which unifies the two under one consistent interface for interacting with language servers. -//! -//! Most of the interesting work happens at the local layer, as bulk of the complexity is with managing the lifecycle of language servers. The actual implementation of the LSP protocol is handled by [`lsp`] crate. pub mod clangd_ext; pub mod json_language_server_ext; pub mod lsp_ext_command; @@ -17,20 +6,20 @@ pub mod rust_analyzer_ext; use crate::{ CodeAction, ColorPresentation, Completion, CompletionResponse, CompletionSource, CoreCompletion, DocumentColor, Hover, InlayHint, LocationLink, LspAction, LspPullDiagnostics, - ManifestProvidersStore, ProjectItem, ProjectPath, ProjectTransaction, PulledDiagnostics, - ResolveState, Symbol, + ProjectItem, ProjectPath, ProjectTransaction, PulledDiagnostics, ResolveState, Symbol, + ToolchainStore, buffer_store::{BufferStore, BufferStoreEvent}, environment::ProjectEnvironment, lsp_command::{self, *}, lsp_store, manifest_tree::{ - LanguageServerTree, LanguageServerTreeNode, LaunchDisposition, ManifestQueryDelegate, - ManifestTree, + AdapterQuery, LanguageServerTree, LanguageServerTreeNode, LaunchDisposition, + ManifestQueryDelegate, ManifestTree, }, prettier_store::{self, PrettierStore, PrettierStoreEvent}, project_settings::{LspSettings, ProjectSettings}, relativize_path, resolve_path, - toolchain_store::{LocalToolchainStore, ToolchainStoreEvent}, + toolchain_store::{EmptyToolchainStore, ToolchainStoreEvent}, worktree_store::{WorktreeStore, WorktreeStoreEvent}, yarn::YarnPathStore, }; @@ -55,9 +44,9 @@ use itertools::Itertools as _; use language::{ Bias, BinaryStatus, Buffer, BufferSnapshot, CachedLspAdapter, CodeLabel, Diagnostic, DiagnosticEntry, DiagnosticSet, DiagnosticSourceKind, Diff, File as _, Language, LanguageName, - LanguageRegistry, LocalFile, LspAdapter, LspAdapterDelegate, ManifestDelegate, ManifestName, - Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Toolchain, Transaction, - Unclipped, + LanguageRegistry, LanguageToolchainStore, LocalFile, LspAdapter, LspAdapterDelegate, Patch, + PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped, + WorkspaceFoldersContent, language_settings::{ FormatOnSave, Formatter, LanguageSettings, SelectedFormatter, language_settings, }, @@ -69,13 +58,12 @@ use language::{ range_from_lsp, range_to_lsp, }; use lsp::{ - AdapterServerCapabilities, CodeActionKind, CompletionContext, DiagnosticSeverity, - DiagnosticTag, DidChangeWatchedFilesRegistrationOptions, Edit, FileOperationFilter, - FileOperationPatternKind, FileOperationRegistrationOptions, FileRename, FileSystemWatcher, - LSP_REQUEST_TIMEOUT, LanguageServer, LanguageServerBinary, LanguageServerBinaryOptions, - LanguageServerId, LanguageServerName, LanguageServerSelector, LspRequestFuture, - MessageActionItem, MessageType, OneOf, RenameFilesParams, SymbolKind, - TextDocumentSyncSaveOptions, TextEdit, WillRenameFiles, WorkDoneProgressCancelParams, + CodeActionKind, CompletionContext, DiagnosticSeverity, DiagnosticTag, + DidChangeWatchedFilesRegistrationOptions, Edit, FileOperationFilter, FileOperationPatternKind, + FileOperationRegistrationOptions, FileRename, FileSystemWatcher, LanguageServer, + LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerId, LanguageServerName, + LanguageServerSelector, LspRequestFuture, MessageActionItem, MessageType, OneOf, + RenameFilesParams, SymbolKind, TextEdit, WillRenameFiles, WorkDoneProgressCancelParams, WorkspaceFolder, notification::DidRenameFiles, }; use node_runtime::read_package_installed_version; @@ -85,7 +73,7 @@ use rand::prelude::*; use rpc::{ AnyProtoClient, - proto::{FromProto, LspRequestId, LspRequestMessage as _, ToProto}, + proto::{FromProto, ToProto}, }; use serde::Serialize; use settings::{Settings, SettingsLocation, SettingsStore}; @@ -93,7 +81,7 @@ use sha2::{Digest, Sha256}; use smol::channel::Sender; use snippet::Snippet; use std::{ - any::{Any, TypeId}, + any::Any, borrow::Cow, cell::RefCell, cmp::{Ordering, Reverse}, @@ -108,7 +96,6 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; -use sum_tree::Dimensions; use text::{Anchor, BufferId, LineEnding, OffsetRangeExt}; use url::Url; use util::{ @@ -152,49 +139,20 @@ impl FormatTrigger { } } -#[derive(Clone)] -struct UnifiedLanguageServer { - id: LanguageServerId, - project_roots: HashSet>, -} - -#[derive(Clone, Hash, PartialEq, Eq)] -struct LanguageServerSeed { - worktree_id: WorktreeId, - name: LanguageServerName, - toolchain: Option, - settings: Arc, -} - -#[derive(Debug)] -pub struct DocumentDiagnosticsUpdate<'a, D> { - pub diagnostics: D, - pub result_id: Option, - pub server_id: LanguageServerId, - pub disk_based_sources: Cow<'a, [String]>, -} - -pub struct DocumentDiagnostics { - diagnostics: Vec>>, - document_abs_path: PathBuf, - version: Option, -} - pub struct LocalLspStore { weak: WeakEntity, worktree_store: Entity, - toolchain_store: Entity, + toolchain_store: Entity, http_client: Arc, environment: Entity, fs: Arc, languages: Arc, - language_server_ids: HashMap, + language_server_ids: HashMap<(WorktreeId, LanguageServerName), BTreeSet>, yarn: Entity, pub language_servers: HashMap, buffers_being_formatted: HashSet, last_workspace_edits_by_language_server: HashMap, language_server_watched_paths: HashMap, - watched_manifest_filenames: HashSet, language_server_paths_watched_for_rename: HashMap, language_server_watcher_registrations: @@ -215,7 +173,7 @@ pub struct LocalLspStore { >, buffer_snapshots: HashMap>>, // buffer_id -> server_id -> vec of snapshots _subscription: gpui::Subscription, - lsp_tree: LanguageServerTree, + lsp_tree: Entity, registered_buffers: HashMap, buffers_opened_in_servers: HashMap>, buffer_pull_diagnostics_result_ids: HashMap>>, @@ -235,81 +193,30 @@ impl LocalLspStore { } } - fn get_or_insert_language_server( - &mut self, - worktree_handle: &Entity, - delegate: Arc, - disposition: &Arc, - language_name: &LanguageName, - cx: &mut App, - ) -> LanguageServerId { - let key = LanguageServerSeed { - worktree_id: worktree_handle.read(cx).id(), - name: disposition.server_name.clone(), - settings: disposition.settings.clone(), - toolchain: disposition.toolchain.clone(), - }; - if let Some(state) = self.language_server_ids.get_mut(&key) { - state.project_roots.insert(disposition.path.path.clone()); - state.id - } else { - let adapter = self - .languages - .lsp_adapters(language_name) - .into_iter() - .find(|adapter| adapter.name() == disposition.server_name) - .expect("To find LSP adapter"); - let new_language_server_id = self.start_language_server( - worktree_handle, - delegate, - adapter, - disposition.settings.clone(), - key.clone(), - cx, - ); - if let Some(state) = self.language_server_ids.get_mut(&key) { - state.project_roots.insert(disposition.path.path.clone()); - } else { - debug_assert!( - false, - "Expected `start_language_server` to ensure that `key` exists in a map" - ); - } - new_language_server_id - } - } - fn start_language_server( &mut self, worktree_handle: &Entity, delegate: Arc, adapter: Arc, settings: Arc, - key: LanguageServerSeed, cx: &mut App, ) -> LanguageServerId { let worktree = worktree_handle.read(cx); - + let worktree_id = worktree.id(); let root_path = worktree.abs_path(); - let toolchain = key.toolchain.clone(); + let key = (worktree_id, adapter.name.clone()); + let override_options = settings.initialization_options.clone(); let stderr_capture = Arc::new(Mutex::new(Some(String::new()))); let server_id = self.languages.next_language_server_id(); - log::trace!( + log::info!( "attempting to start language server {:?}, path: {root_path:?}, id: {server_id}", adapter.name.0 ); - let binary = self.get_language_server_binary( - adapter.clone(), - settings, - toolchain.clone(), - delegate.clone(), - true, - cx, - ); + let binary = self.get_language_server_binary(adapter.clone(), delegate.clone(), true, cx); let pending_workspace_folders: Arc>> = Default::default(); let pending_server = cx.spawn({ @@ -345,7 +252,10 @@ impl LocalLspStore { binary, &root_path, code_action_kinds, - Some(pending_workspace_folders), + Some(pending_workspace_folders).filter(|_| { + adapter.adapter.workspace_folders_content() + == WorkspaceFoldersContent::SubprojectRoots + }), cx, ) } @@ -365,13 +275,15 @@ impl LocalLspStore { .enabled; cx.spawn(async move |cx| { let result = async { + let toolchains = + lsp_store.update(cx, |lsp_store, cx| lsp_store.toolchain_store(cx))?; let language_server = pending_server.await?; let workspace_config = Self::workspace_configuration_for_adapter( adapter.adapter.clone(), fs.as_ref(), &delegate, - toolchain, + toolchains.clone(), cx, ) .await?; @@ -443,14 +355,14 @@ impl LocalLspStore { match result { Ok(server) => { lsp_store - .update(cx, |lsp_store, cx| { + .update(cx, |lsp_store, mut cx| { lsp_store.insert_newly_running_language_server( adapter, server.clone(), server_id, key, pending_workspace_folders, - cx, + &mut cx, ); }) .ok(); @@ -463,17 +375,13 @@ impl LocalLspStore { delegate.update_status( adapter.name(), BinaryStatus::Failed { - error: if log.is_empty() { - format!("{err:#}") - } else { - format!("{err:#}\n-- stderr --\n{log}") - }, + error: format!("{err}\n-- stderr--\n{log}"), }, ); - log::error!("Failed to start language server {server_name:?}: {err:?}"); - if !log.is_empty() { - log::error!("server stderr: {log}"); - } + let message = + format!("Failed to start language server {server_name:?}: {err:#?}"); + log::error!("{message}"); + log::error!("server stderr: {log}"); None } } @@ -490,26 +398,31 @@ impl LocalLspStore { self.language_servers.insert(server_id, state); self.language_server_ids .entry(key) - .or_insert(UnifiedLanguageServer { - id: server_id, - project_roots: Default::default(), - }); + .or_default() + .insert(server_id); server_id } fn get_language_server_binary( &self, adapter: Arc, - settings: Arc, - toolchain: Option, delegate: Arc, allow_binary_download: bool, cx: &mut App, ) -> Task> { - if let Some(settings) = settings.binary.as_ref() - && settings.path.is_some() - { - let settings = settings.clone(); + let settings = ProjectSettings::get( + Some(SettingsLocation { + worktree_id: delegate.worktree_id(), + path: Path::new(""), + }), + cx, + ) + .lsp + .get(&adapter.name) + .and_then(|s| s.binary.clone()); + + if settings.as_ref().is_some_and(|b| b.path.is_some()) { + let settings = settings.unwrap(); return cx.background_spawn(async move { let mut env = delegate.shell_env().await; @@ -529,17 +442,16 @@ impl LocalLspStore { } let lsp_binary_options = LanguageServerBinaryOptions { allow_path_lookup: !settings - .binary .as_ref() .and_then(|b| b.ignore_system_version) .unwrap_or_default(), allow_binary_download, }; - + let toolchains = self.toolchain_store.read(cx).as_language_toolchain_store(); cx.spawn(async move |cx| { let binary_result = adapter .clone() - .get_language_server_command(delegate.clone(), toolchain, lsp_binary_options, cx) + .get_language_server_command(delegate.clone(), toolchains, lsp_binary_options, cx) .await; delegate.update_status(adapter.name.clone(), BinaryStatus::None); @@ -549,12 +461,12 @@ impl LocalLspStore { shell_env.extend(binary.env.unwrap_or_default()); - if let Some(settings) = settings.binary.as_ref() { - if let Some(arguments) = &settings.arguments { - binary.arguments = arguments.iter().map(Into::into).collect(); + if let Some(settings) = settings { + if let Some(arguments) = settings.arguments { + binary.arguments = arguments.into_iter().map(Into::into).collect(); } - if let Some(env) = &settings.env { - shell_env.extend(env.iter().map(|(k, v)| (k.clone(), v.clone()))); + if let Some(env) = settings.env { + shell_env.extend(env); } } @@ -590,16 +502,12 @@ impl LocalLspStore { adapter.process_diagnostics(&mut params, server_id, buffer); } - this.merge_lsp_diagnostics( + this.merge_diagnostics( + server_id, + params, + None, DiagnosticSourceKind::Pushed, - vec![DocumentDiagnosticsUpdate { - server_id, - diagnostics: params, - result_id: None, - disk_based_sources: Cow::Borrowed( - &adapter.disk_based_diagnostic_sources, - ), - }], + &adapter.disk_based_diagnostic_sources, |_, diagnostic, cx| match diagnostic.source_kind { DiagnosticSourceKind::Other | DiagnosticSourceKind::Pushed => { adapter.retain_old_diagnostic(diagnostic, cx) @@ -628,20 +536,14 @@ impl LocalLspStore { let fs = fs.clone(); let mut cx = cx.clone(); async move { - let toolchain_for_id = this - .update(&mut cx, |this, _| { - this.as_local()?.language_server_ids.iter().find_map( - |(seed, value)| { - (value.id == server_id).then(|| seed.toolchain.clone()) - }, - ) - })? - .context("Expected the LSP store to be in a local mode")?; + let toolchains = + this.update(&mut cx, |this, cx| this.toolchain_store(cx))?; + let workspace_config = Self::workspace_configuration_for_adapter( adapter.clone(), fs.as_ref(), &delegate, - toolchain_for_id, + toolchains.clone(), &mut cx, ) .await?; @@ -670,10 +572,10 @@ impl LocalLspStore { let this = this.clone(); move |_, cx| { let this = this.clone(); - let cx = cx.clone(); + let mut cx = cx.clone(); async move { - let Some(server) = - this.read_with(&cx, |this, _| this.language_server_for_id(server_id))? + let Some(server) = this + .read_with(&mut cx, |this, _| this.language_server_for_id(server_id))? else { return Ok(None); }; @@ -702,9 +604,10 @@ impl LocalLspStore { async move { this.update(&mut cx, |this, _| { if let Some(status) = this.language_server_statuses.get_mut(&server_id) - && let lsp::NumberOrString::String(token) = params.token { - status.progress_tokens.insert(token); + if let lsp::NumberOrString::String(token) = params.token { + status.progress_tokens.insert(token); + } } })?; @@ -716,27 +619,131 @@ impl LocalLspStore { language_server .on_request::({ - let lsp_store = this.clone(); + let this = this.clone(); move |params, cx| { - let lsp_store = lsp_store.clone(); + let this = this.clone(); let mut cx = cx.clone(); async move { - lsp_store - .update(&mut cx, |lsp_store, cx| { - if lsp_store.as_local().is_some() { - match lsp_store - .register_server_capabilities(server_id, params, cx) - { - Ok(()) => {} - Err(e) => { - log::error!( - "Failed to register server capabilities: {e:#}" + for reg in params.registrations { + match reg.method.as_str() { + "workspace/didChangeWatchedFiles" => { + if let Some(options) = reg.register_options { + let options = serde_json::from_value(options)?; + this.update(&mut cx, |this, cx| { + this.as_local_mut()?.on_lsp_did_change_watched_files( + server_id, ®.id, options, cx, ); - } - }; + Some(()) + })?; + } } - }) - .ok(); + "textDocument/rangeFormatting" => { + this.read_with(&mut cx, |this, _| { + if let Some(server) = this.language_server_for_id(server_id) + { + let options = reg + .register_options + .map(|options| { + serde_json::from_value::< + lsp::DocumentRangeFormattingOptions, + >( + options + ) + }) + .transpose()?; + let provider = match options { + None => OneOf::Left(true), + Some(options) => OneOf::Right(options), + }; + server.update_capabilities(|capabilities| { + capabilities.document_range_formatting_provider = + Some(provider); + }) + } + anyhow::Ok(()) + })??; + } + "textDocument/onTypeFormatting" => { + this.read_with(&mut cx, |this, _| { + if let Some(server) = this.language_server_for_id(server_id) + { + let options = reg + .register_options + .map(|options| { + serde_json::from_value::< + lsp::DocumentOnTypeFormattingOptions, + >( + options + ) + }) + .transpose()?; + if let Some(options) = options { + server.update_capabilities(|capabilities| { + capabilities + .document_on_type_formatting_provider = + Some(options); + }) + } + } + anyhow::Ok(()) + })??; + } + "textDocument/formatting" => { + this.read_with(&mut cx, |this, _| { + if let Some(server) = this.language_server_for_id(server_id) + { + let options = reg + .register_options + .map(|options| { + serde_json::from_value::< + lsp::DocumentFormattingOptions, + >( + options + ) + }) + .transpose()?; + let provider = match options { + None => OneOf::Left(true), + Some(options) => OneOf::Right(options), + }; + server.update_capabilities(|capabilities| { + capabilities.document_formatting_provider = + Some(provider); + }) + } + anyhow::Ok(()) + })??; + } + "workspace/didChangeConfiguration" => { + // Ignore payload since we notify clients of setting changes unconditionally, relying on them pulling the latest settings. + } + "textDocument/rename" => { + this.read_with(&mut cx, |this, _| { + if let Some(server) = this.language_server_for_id(server_id) + { + let options = reg + .register_options + .map(|options| { + serde_json::from_value::( + options, + ) + }) + .transpose()?; + let options = match options { + None => OneOf::Left(true), + Some(options) => OneOf::Right(options), + }; + + server.update_capabilities(|capabilities| { + capabilities.rename_provider = Some(options); + }) + } + anyhow::Ok(()) + })??; + } + _ => log::warn!("unhandled capability registration: {reg:?}"), + } + } Ok(()) } } @@ -745,27 +752,70 @@ impl LocalLspStore { language_server .on_request::({ - let lsp_store = this.clone(); + let this = this.clone(); move |params, cx| { - let lsp_store = lsp_store.clone(); + let this = this.clone(); let mut cx = cx.clone(); async move { - lsp_store - .update(&mut cx, |lsp_store, cx| { - if lsp_store.as_local().is_some() { - match lsp_store - .unregister_server_capabilities(server_id, params, cx) - { - Ok(()) => {} - Err(e) => { - log::error!( - "Failed to unregister server capabilities: {e:#}" + for unreg in params.unregisterations.iter() { + match unreg.method.as_str() { + "workspace/didChangeWatchedFiles" => { + this.update(&mut cx, |this, cx| { + this.as_local_mut()? + .on_lsp_unregister_did_change_watched_files( + server_id, &unreg.id, cx, ); - } - } + Some(()) + })?; } - }) - .ok(); + "workspace/didChangeConfiguration" => { + // Ignore payload since we notify clients of setting changes unconditionally, relying on them pulling the latest settings. + } + "textDocument/rename" => { + this.read_with(&mut cx, |this, _| { + if let Some(server) = this.language_server_for_id(server_id) + { + server.update_capabilities(|capabilities| { + capabilities.rename_provider = None + }) + } + })?; + } + "textDocument/rangeFormatting" => { + this.read_with(&mut cx, |this, _| { + if let Some(server) = this.language_server_for_id(server_id) + { + server.update_capabilities(|capabilities| { + capabilities.document_range_formatting_provider = + None + }) + } + })?; + } + "textDocument/onTypeFormatting" => { + this.read_with(&mut cx, |this, _| { + if let Some(server) = this.language_server_for_id(server_id) + { + server.update_capabilities(|capabilities| { + capabilities.document_on_type_formatting_provider = + None; + }) + } + })?; + } + "textDocument/formatting" => { + this.read_with(&mut cx, |this, _| { + if let Some(server) = this.language_server_for_id(server_id) + { + server.update_capabilities(|capabilities| { + capabilities.document_formatting_provider = None; + }) + } + })?; + } + _ => log::warn!("unhandled capability unregistration: {unreg:?}"), + } + } Ok(()) } } @@ -774,15 +824,18 @@ impl LocalLspStore { language_server .on_request::({ + let adapter = adapter.clone(); let this = this.clone(); move |params, cx| { let mut cx = cx.clone(); let this = this.clone(); + let adapter = adapter.clone(); async move { LocalLspStore::on_lsp_workspace_edit( this.clone(), params, server_id, + adapter.clone(), &mut cx, ) .await @@ -918,7 +971,7 @@ impl LocalLspStore { message: params.message, actions: vec![], response_channel: tx, - lsp_name: name, + lsp_name: name.clone(), }; let _ = this.update(&mut cx, |_, cx| { @@ -1015,10 +1068,10 @@ impl LocalLspStore { } } LanguageServerState::Starting { startup, .. } => { - if let Some(server) = startup.await - && let Some(shutdown) = server.shutdown() - { - shutdown.await; + if let Some(server) = startup.await { + if let Some(shutdown) = server.shutdown() { + shutdown.await; + } } } } @@ -1031,18 +1084,19 @@ impl LocalLspStore { ) -> impl Iterator> { self.language_server_ids .iter() - .filter_map(move |(seed, state)| { - if seed.worktree_id != worktree_id { - return None; - } - - if let Some(LanguageServerState::Running { server, .. }) = - self.language_servers.get(&state.id) - { - Some(server) - } else { - None - } + .flat_map(move |((language_server_path, _), ids)| { + ids.iter().filter_map(move |id| { + if *language_server_path != worktree_id { + return None; + } + if let Some(LanguageServerState::Running { server, .. }) = + self.language_servers.get(id) + { + return Some(server); + } else { + None + } + }) }) } @@ -1059,18 +1113,19 @@ impl LocalLspStore { else { return Vec::new(); }; - let delegate: Arc = - Arc::new(ManifestQueryDelegate::new(worktree.read(cx).snapshot())); - - self.lsp_tree - .get( + let delegate = Arc::new(ManifestQueryDelegate::new(worktree.read(cx).snapshot())); + let root = self.lsp_tree.update(cx, |this, cx| { + this.get( project_path, - language.name(), - language.manifest(), - &delegate, + AdapterQuery::Language(&language.name()), + delegate, cx, ) + .filter_map(|node| node.server_id()) .collect::>() + }); + + root } fn language_server_ids_for_buffer( @@ -1152,7 +1207,7 @@ impl LocalLspStore { .collect::>() }) })?; - for (_, language_server) in adapters_and_servers.iter() { + for (lsp_adapter, language_server) in adapters_and_servers.iter() { let actions = Self::get_server_code_actions_from_action_kinds( &lsp_store, language_server.server_id(), @@ -1164,6 +1219,7 @@ impl LocalLspStore { Self::execute_code_actions_on_server( &lsp_store, language_server, + lsp_adapter, actions, push_to_history, &mut project_transaction, @@ -1878,7 +1934,7 @@ impl LocalLspStore { ) -> Result, Arc)>> { let capabilities = &language_server.capabilities(); let range_formatting_provider = capabilities.document_range_formatting_provider.as_ref(); - if range_formatting_provider == Some(&OneOf::Left(false)) { + if range_formatting_provider.map_or(false, |provider| provider == &OneOf::Left(false)) { anyhow::bail!( "{} language server does not support range formatting", language_server.name() @@ -1925,7 +1981,7 @@ impl LocalLspStore { if let Some(lsp_edits) = lsp_edits { this.update(cx, |this, cx| { this.as_local_mut().unwrap().edits_from_lsp( - buffer_handle, + &buffer_handle, lsp_edits, language_server.server_id(), None, @@ -2106,14 +2162,13 @@ impl LocalLspStore { let buffer = buffer_handle.read(cx); let file = buffer.file().cloned(); - let Some(file) = File::from_dyn(file.as_ref()) else { return; }; if !file.is_local() { return; } - let path = ProjectPath::from_file(file, cx); + let worktree_id = file.worktree_id(cx); let language = buffer.language().cloned(); @@ -2136,52 +2191,46 @@ impl LocalLspStore { let Some(language) = language else { return; }; - let Some(snapshot) = self - .worktree_store - .read(cx) - .worktree_for_id(worktree_id, cx) - .map(|worktree| worktree.read(cx).snapshot()) - else { - return; - }; - let delegate: Arc = Arc::new(ManifestQueryDelegate::new(snapshot)); + for adapter in self.languages.lsp_adapters(&language.name()) { + let servers = self + .language_server_ids + .get(&(worktree_id, adapter.name.clone())); + if let Some(server_ids) = servers { + for server_id in server_ids { + let server = self + .language_servers + .get(server_id) + .and_then(|server_state| { + if let LanguageServerState::Running { server, .. } = server_state { + Some(server.clone()) + } else { + None + } + }); + let server = match server { + Some(server) => server, + None => continue, + }; - for server_id in - self.lsp_tree - .get(path, language.name(), language.manifest(), &delegate, cx) - { - let server = self - .language_servers - .get(&server_id) - .and_then(|server_state| { - if let LanguageServerState::Running { server, .. } = server_state { - Some(server.clone()) - } else { - None - } - }); - let server = match server { - Some(server) => server, - None => continue, - }; - - buffer_handle.update(cx, |buffer, cx| { - buffer.set_completion_triggers( - server.server_id(), - server - .capabilities() - .completion_provider - .as_ref() - .and_then(|provider| { - provider - .trigger_characters + buffer_handle.update(cx, |buffer, cx| { + buffer.set_completion_triggers( + server.server_id(), + server + .capabilities() + .completion_provider .as_ref() - .map(|characters| characters.iter().cloned().collect()) - }) - .unwrap_or_default(), - cx, - ); - }); + .and_then(|provider| { + provider + .trigger_characters + .as_ref() + .map(|characters| characters.iter().cloned().collect()) + }) + .unwrap_or_default(), + cx, + ); + }); + } + } } } @@ -2291,31 +2340,6 @@ impl LocalLspStore { Ok(()) } - fn register_language_server_for_invisible_worktree( - &mut self, - worktree: &Entity, - language_server_id: LanguageServerId, - cx: &mut App, - ) { - let worktree = worktree.read(cx); - let worktree_id = worktree.id(); - debug_assert!(!worktree.is_visible()); - let Some(mut origin_seed) = self - .language_server_ids - .iter() - .find_map(|(seed, state)| (state.id == language_server_id).then(|| seed.clone())) - else { - return; - }; - origin_seed.worktree_id = worktree_id; - self.language_server_ids - .entry(origin_seed) - .or_insert_with(|| UnifiedLanguageServer { - id: language_server_id, - project_roots: Default::default(), - }); - } - fn register_buffer_with_language_servers( &mut self, buffer_handle: &Entity, @@ -2356,23 +2380,27 @@ impl LocalLspStore { }; let language_name = language.name(); let (reused, delegate, servers) = self - .reuse_existing_language_server(&self.lsp_tree, &worktree, &language_name, cx) - .map(|(delegate, apply)| (true, delegate, apply(&mut self.lsp_tree))) + .lsp_tree + .update(cx, |lsp_tree, cx| { + self.reuse_existing_language_server(lsp_tree, &worktree, &language_name, cx) + }) + .map(|(delegate, servers)| (true, delegate, servers)) .unwrap_or_else(|| { let lsp_delegate = LocalLspAdapterDelegate::from_local_lsp(self, &worktree, cx); - let delegate: Arc = - Arc::new(ManifestQueryDelegate::new(worktree.read(cx).snapshot())); - + let delegate = Arc::new(ManifestQueryDelegate::new(worktree.read(cx).snapshot())); let servers = self .lsp_tree - .walk( - ProjectPath { worktree_id, path }, - language.name(), - language.manifest(), - &delegate, - cx, - ) - .collect::>(); + .clone() + .update(cx, |language_server_tree, cx| { + language_server_tree + .get( + ProjectPath { worktree_id, path }, + AdapterQuery::Language(&language.name()), + delegate.clone(), + cx, + ) + .collect::>() + }); (false, lsp_delegate, servers) }); let servers_and_adapters = servers @@ -2382,46 +2410,80 @@ impl LocalLspStore { return None; } if !only_register_servers.is_empty() { - if let Some(server_id) = server_node.server_id() - && !only_register_servers.contains(&LanguageServerSelector::Id(server_id)) - { - return None; + if let Some(server_id) = server_node.server_id() { + if !only_register_servers.contains(&LanguageServerSelector::Id(server_id)) { + return None; + } } - if let Some(name) = server_node.name() - && !only_register_servers.contains(&LanguageServerSelector::Name(name)) - { - return None; + if let Some(name) = server_node.name() { + if !only_register_servers.contains(&LanguageServerSelector::Name(name)) { + return None; + } } } - let server_id = server_node.server_id_or_init(|disposition| { - let path = &disposition.path; + let server_id = server_node.server_id_or_init( + |LaunchDisposition { + server_name, - { - let uri = - Url::from_file_path(worktree.read(cx).abs_path().join(&path.path)); + path, + settings, + }| { + let server_id = + { + let uri = Url::from_file_path( + worktree.read(cx).abs_path().join(&path.path), + ); + let key = (worktree_id, server_name.clone()); + if !self.language_server_ids.contains_key(&key) { + let language_name = language.name(); + let adapter = self.languages + .lsp_adapters(&language_name) + .into_iter() + .find(|adapter| &adapter.name() == server_name) + .expect("To find LSP adapter"); + self.start_language_server( + &worktree, + delegate.clone(), + adapter, + settings, + cx, + ); + } + if let Some(server_ids) = self + .language_server_ids + .get(&key) + { + debug_assert_eq!(server_ids.len(), 1); + let server_id = server_ids.iter().cloned().next().unwrap(); + if let Some(state) = self.language_servers.get(&server_id) { + if let Ok(uri) = uri { + state.add_workspace_folder(uri); + }; + } + server_id + } else { + unreachable!("Language server ID should be available, as it's registered on demand") + } - let server_id = self.get_or_insert_language_server( - &worktree, - delegate.clone(), - disposition, - &language_name, - cx, - ); - - if let Some(state) = self.language_servers.get(&server_id) - && let Ok(uri) = uri - { - state.add_workspace_folder(uri); }; + let lsp_store = self.weak.clone(); + let server_name = server_node.name(); + let buffer_abs_path = abs_path.to_string_lossy().to_string(); + cx.defer(move |cx| { + lsp_store.update(cx, |_, cx| cx.emit(LspStoreEvent::LanguageServerUpdate { + language_server_id: server_id, + name: server_name, + message: proto::update_language_server::Variant::RegisteredForBuffer(proto::RegisteredForBuffer { + buffer_abs_path, + }) + })).ok(); + }); server_id - } - })?; + }, + )?; let server_state = self.language_servers.get(&server_id)?; - if let LanguageServerState::Running { - server, adapter, .. - } = server_state - { + if let LanguageServerState::Running { server, adapter, .. } = server_state { Some((server.clone(), adapter.clone())) } else { None @@ -2452,13 +2514,11 @@ impl LocalLspStore { snapshot: initial_snapshot.clone(), }; - let mut registered = false; self.buffer_snapshots .entry(buffer_id) .or_default() .entry(server.server_id()) .or_insert_with(|| { - registered = true; server.register_buffer( uri.clone(), adapter.language_id(&language.name()), @@ -2473,31 +2533,25 @@ impl LocalLspStore { .entry(buffer_id) .or_default() .insert(server.server_id()); - if registered { - cx.emit(LspStoreEvent::LanguageServerUpdate { - language_server_id: server.server_id(), - name: None, - message: proto::update_language_server::Variant::RegisteredForBuffer( - proto::RegisteredForBuffer { - buffer_abs_path: abs_path.to_string_lossy().to_string(), - buffer_id: buffer_id.to_proto(), - }, - ), - }); - } + cx.emit(LspStoreEvent::LanguageServerUpdate { + language_server_id: server.server_id(), + name: None, + message: proto::update_language_server::Variant::RegisteredForBuffer( + proto::RegisteredForBuffer { + buffer_abs_path: abs_path.to_string_lossy().to_string(), + }, + ), + }); } } - fn reuse_existing_language_server<'lang_name>( + fn reuse_existing_language_server( &self, - server_tree: &LanguageServerTree, + server_tree: &mut LanguageServerTree, worktree: &Entity, - language_name: &'lang_name LanguageName, + language_name: &LanguageName, cx: &mut App, - ) -> Option<( - Arc, - impl FnOnce(&mut LanguageServerTree) -> Vec + use<'lang_name>, - )> { + ) -> Option<(Arc, Vec)> { if worktree.read(cx).is_visible() { return None; } @@ -2536,16 +2590,16 @@ impl LocalLspStore { .into_values() .max_by_key(|servers| servers.len())?; - let worktree_id = worktree.read(cx).id(); - let apply = move |tree: &mut LanguageServerTree| { - for server_node in &servers { - tree.register_reused(worktree_id, language_name.clone(), server_node.clone()); - } - servers - }; + for server_node in &servers { + server_tree.register_reused( + worktree.read(cx).id(), + language_name.clone(), + server_node.clone(), + ); + } let delegate = LocalLspAdapterDelegate::from_local_lsp(self, worktree, cx); - Some((delegate, apply)) + Some((delegate, servers)) } pub(crate) fn unregister_old_buffer_from_language_servers( @@ -2640,13 +2694,13 @@ impl LocalLspStore { this.request_lsp(buffer.clone(), server, request, cx) })? .await?; - Ok(actions) + return Ok(actions); } pub async fn execute_code_actions_on_server( lsp_store: &WeakEntity, language_server: &Arc, - + lsp_adapter: &Arc, actions: Vec, push_to_history: bool, project_transaction: &mut ProjectTransaction, @@ -2666,6 +2720,7 @@ impl LocalLspStore { lsp_store.upgrade().context("project dropped")?, edit.clone(), push_to_history, + lsp_adapter.clone(), language_server.clone(), cx, ) @@ -2716,7 +2771,7 @@ impl LocalLspStore { } } } - Ok(()) + return Ok(()); } pub async fn deserialize_text_edits( @@ -2846,6 +2901,7 @@ impl LocalLspStore { this: Entity, edit: lsp::WorkspaceEdit, push_to_history: bool, + lsp_adapter: Arc, language_server: Arc, cx: &mut AsyncApp, ) -> Result { @@ -2946,6 +3002,7 @@ impl LocalLspStore { this.open_local_buffer_via_lsp( op.text_document.uri.clone(), language_server.server_id(), + lsp_adapter.name.clone(), cx, ) })? @@ -2955,11 +3012,11 @@ impl LocalLspStore { .update(cx, |this, cx| { let path = buffer_to_edit.read(cx).project_path(cx); let active_entry = this.active_entry; - let is_active_entry = path.is_some_and(|project_path| { + let is_active_entry = path.clone().map_or(false, |project_path| { this.worktree_store .read(cx) .entry_for_path(&project_path, cx) - .is_some_and(|entry| Some(entry.id) == active_entry) + .map_or(false, |entry| Some(entry.id) == active_entry) }); let local = this.as_local_mut().unwrap(); @@ -3045,14 +3102,16 @@ impl LocalLspStore { buffer.edit([(range, text)], None, cx); } - buffer.end_transaction(cx).and_then(|transaction_id| { + let transaction = buffer.end_transaction(cx).and_then(|transaction_id| { if push_to_history { buffer.finalize_last_transaction(); buffer.get_transaction(transaction_id).cloned() } else { buffer.forget_transaction(transaction_id) } - }) + }); + + transaction })?; if let Some(transaction) = transaction { project_transaction.0.insert(buffer_to_edit, transaction); @@ -3068,6 +3127,7 @@ impl LocalLspStore { this: WeakEntity, params: lsp::ApplyWorkspaceEditParams, server_id: LanguageServerId, + adapter: Arc, cx: &mut AsyncApp, ) -> Result { let this = this.upgrade().context("project project closed")?; @@ -3078,6 +3138,7 @@ impl LocalLspStore { this.clone(), params.edit, true, + adapter.clone(), language_server.clone(), cx, ) @@ -3108,19 +3169,23 @@ impl LocalLspStore { prettier_store.remove_worktree(id_to_remove, cx); }); - let mut servers_to_remove = BTreeSet::default(); + let mut servers_to_remove = BTreeMap::default(); let mut servers_to_preserve = HashSet::default(); - for (seed, state) in &self.language_server_ids { - if seed.worktree_id == id_to_remove { - servers_to_remove.insert(state.id); + for ((path, server_name), ref server_ids) in &self.language_server_ids { + if *path == id_to_remove { + servers_to_remove.extend(server_ids.iter().map(|id| (*id, server_name.clone()))); } else { - servers_to_preserve.insert(state.id); + servers_to_preserve.extend(server_ids.iter().cloned()); } } - servers_to_remove.retain(|server_id| !servers_to_preserve.contains(server_id)); - self.language_server_ids - .retain(|_, state| !servers_to_remove.contains(&state.id)); - for server_id_to_remove in &servers_to_remove { + servers_to_remove.retain(|server_id, _| !servers_to_preserve.contains(server_id)); + + for (server_id_to_remove, _) in &servers_to_remove { + self.language_server_ids + .values_mut() + .for_each(|server_ids| { + server_ids.remove(server_id_to_remove); + }); self.language_server_watched_paths .remove(server_id_to_remove); self.language_server_paths_watched_for_rename @@ -3135,7 +3200,7 @@ impl LocalLspStore { } cx.emit(LspStoreEvent::LanguageServerRemoved(*server_id_to_remove)); } - servers_to_remove.into_iter().collect() + servers_to_remove.into_keys().collect() } fn rebuild_watched_paths_inner<'a>( @@ -3164,7 +3229,7 @@ impl LocalLspStore { for watcher in watchers { if let Some((worktree, literal_prefix, pattern)) = - self.worktree_and_path_for_file_watcher(&worktrees, watcher, cx) + self.worktree_and_path_for_file_watcher(&worktrees, &watcher, cx) { worktree.update(cx, |worktree, _| { if let Some((tree, glob)) = @@ -3393,20 +3458,16 @@ impl LocalLspStore { Ok(Some(initialization_config)) } - fn toolchain_store(&self) -> &Entity { - &self.toolchain_store - } - async fn workspace_configuration_for_adapter( adapter: Arc, fs: &dyn Fs, delegate: &Arc, - toolchain: Option, + toolchains: Arc, cx: &mut AsyncApp, ) -> Result { let mut workspace_config = adapter .clone() - .workspace_configuration(fs, delegate, toolchain, cx) + .workspace_configuration(fs, delegate, toolchains.clone(), cx) .await?; for other_adapter in delegate.registered_lsp_adapters() { @@ -3415,7 +3476,13 @@ impl LocalLspStore { } if let Ok(Some(target_config)) = other_adapter .clone() - .additional_workspace_configuration(adapter.name(), fs, delegate, cx) + .additional_workspace_configuration( + adapter.name(), + fs, + delegate, + toolchains.clone(), + cx, + ) .await { merge_json_value_into(target_config.clone(), &mut workspace_config); @@ -3424,30 +3491,6 @@ impl LocalLspStore { Ok(workspace_config) } - - fn language_server_for_id(&self, id: LanguageServerId) -> Option> { - if let Some(LanguageServerState::Running { server, .. }) = self.language_servers.get(&id) { - Some(server.clone()) - } else if let Some((_, server)) = self.supplementary_language_servers.get(&id) { - Some(Arc::clone(server)) - } else { - None - } - } -} - -fn notify_server_capabilities_updated(server: &LanguageServer, cx: &mut Context) { - if let Some(capabilities) = serde_json::to_string(&server.capabilities()).ok() { - cx.emit(LspStoreEvent::LanguageServerUpdate { - language_server_id: server.server_id(), - name: Some(server.name()), - message: proto::update_language_server::Variant::MetadataUpdated( - proto::ServerMetadataUpdated { - capabilities: Some(capabilities), - }, - ), - }); - } } #[derive(Debug)] @@ -3481,6 +3524,7 @@ pub struct LspStore { nonce: u128, buffer_store: Entity, worktree_store: Entity, + toolchain_store: Option>, pub languages: Arc, language_server_statuses: BTreeMap, active_entry: Option, @@ -3488,10 +3532,8 @@ pub struct LspStore { _maintain_buffer_languages: Task<()>, diagnostic_summaries: HashMap, HashMap>>, - pub(super) lsp_server_capabilities: HashMap, lsp_document_colors: HashMap, lsp_code_lens: HashMap, - running_lsp_requests: HashMap>)>, } #[derive(Debug, Default, Clone)] @@ -3501,7 +3543,7 @@ pub struct DocumentColors { } type DocumentColorTask = Shared>>>; -type CodeLensTask = Shared>, Arc>>>; +type CodeLensTask = Shared, Arc>>>; #[derive(Debug, Default)] struct DocumentColorData { @@ -3543,8 +3585,8 @@ pub enum LspStoreEvent { RefreshInlayHints, RefreshCodeLens, DiagnosticsUpdated { - server_id: LanguageServerId, - paths: Vec, + language_server_id: LanguageServerId, + path: ProjectPath, }, DiskBasedDiagnosticsStarted { language_server_id: LanguageServerId, @@ -3561,7 +3603,7 @@ pub enum LspStoreEvent { #[derive(Clone, Debug, Serialize)] pub struct LanguageServerStatus { - pub name: LanguageServerName, + pub name: String, pub pending_work: BTreeMap, pub has_pending_diagnostic_updates: bool, progress_tokens: HashSet, @@ -3581,8 +3623,6 @@ struct CoreSymbol { impl LspStore { pub fn init(client: &AnyProtoClient) { - client.add_entity_request_handler(Self::handle_lsp_query); - client.add_entity_message_handler(Self::handle_lsp_query_response); client.add_entity_request_handler(Self::handle_multi_lsp_query); client.add_entity_request_handler(Self::handle_restart_language_servers); client.add_entity_request_handler(Self::handle_stop_language_servers); @@ -3606,6 +3646,7 @@ impl LspStore { client.add_entity_request_handler(Self::handle_apply_additional_edits_for_completion); client.add_entity_request_handler(Self::handle_register_buffer_with_language_servers); client.add_entity_request_handler(Self::handle_rename_project_entry); + client.add_entity_request_handler(Self::handle_language_server_id_for_name); client.add_entity_request_handler(Self::handle_pull_workspace_diagnostics); client.add_entity_request_handler(Self::handle_lsp_command::); client.add_entity_request_handler(Self::handle_lsp_command::); @@ -3674,7 +3715,7 @@ impl LspStore { buffer_store: Entity, worktree_store: Entity, prettier_store: Entity, - toolchain_store: Entity, + toolchain_store: Entity, environment: Entity, manifest_tree: Entity, languages: Arc, @@ -3716,7 +3757,7 @@ impl LspStore { mode: LspStoreMode::Local(LocalLspStore { weak: cx.weak_entity(), worktree_store: worktree_store.clone(), - + toolchain_store: toolchain_store.clone(), supplementary_language_servers: Default::default(), languages: languages.clone(), language_server_ids: Default::default(), @@ -3739,30 +3780,22 @@ impl LspStore { .unwrap() .shutdown_language_servers_on_quit(cx) }), - lsp_tree: LanguageServerTree::new( - manifest_tree, - languages.clone(), - toolchain_store.clone(), - ), - toolchain_store, + lsp_tree: LanguageServerTree::new(manifest_tree, languages.clone(), cx), registered_buffers: HashMap::default(), buffers_opened_in_servers: HashMap::default(), buffer_pull_diagnostics_result_ids: HashMap::default(), - watched_manifest_filenames: ManifestProvidersStore::global(cx) - .manifest_file_names(), }), last_formatting_failure: None, downstream_client: None, buffer_store, worktree_store, + toolchain_store: Some(toolchain_store), languages: languages.clone(), language_server_statuses: Default::default(), nonce: StdRng::from_entropy().r#gen(), diagnostic_summaries: HashMap::default(), - lsp_server_capabilities: HashMap::default(), lsp_document_colors: HashMap::default(), lsp_code_lens: HashMap::default(), - running_lsp_requests: HashMap::default(), active_entry: None, _maintain_workspace_config, _maintain_buffer_languages: Self::maintain_buffer_languages(languages, cx), @@ -3777,9 +3810,6 @@ impl LspStore { request: R, cx: &mut Context, ) -> Task::Response>> { - if !self.is_capable_for_proto_request(&buffer, &request, cx) { - return Task::ready(Ok(R::Response::default())); - } let message = request.to_proto(upstream_project_id, buffer.read(cx)); cx.spawn(async move |this, cx| { let response = client.request(message).await?; @@ -3793,6 +3823,7 @@ impl LspStore { pub(super) fn new_remote( buffer_store: Entity, worktree_store: Entity, + toolchain_store: Option>, languages: Arc, upstream_client: AnyProtoClient, project_id: u64, @@ -3821,12 +3852,10 @@ impl LspStore { language_server_statuses: Default::default(), nonce: StdRng::from_entropy().r#gen(), diagnostic_summaries: HashMap::default(), - lsp_server_capabilities: HashMap::default(), lsp_document_colors: HashMap::default(), lsp_code_lens: HashMap::default(), - running_lsp_requests: HashMap::default(), active_entry: None, - + toolchain_store, _maintain_workspace_config, _maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx), } @@ -3844,13 +3873,13 @@ impl LspStore { } BufferStoreEvent::BufferChangedFilePath { buffer, old_file } => { let buffer_id = buffer.read(cx).remote_id(); - if let Some(local) = self.as_local_mut() - && let Some(old_file) = File::from_dyn(old_file.as_ref()) - { - local.reset_buffer(buffer, old_file, cx); + if let Some(local) = self.as_local_mut() { + if let Some(old_file) = File::from_dyn(old_file.as_ref()) { + local.reset_buffer(buffer, old_file, cx); - if local.registered_buffers.contains_key(&buffer_id) { - local.unregister_old_buffer_from_language_servers(buffer, old_file, cx); + if local.registered_buffers.contains_key(&buffer_id) { + local.unregister_old_buffer_from_language_servers(buffer, old_file, cx); + } } } @@ -3925,12 +3954,14 @@ impl LspStore { fn on_toolchain_store_event( &mut self, - _: Entity, + _: Entity, event: &ToolchainStoreEvent, _: &mut Context, ) { match event { - ToolchainStoreEvent::ToolchainActivated => self.request_workspace_config_refresh(), + ToolchainStoreEvent::ToolchainActivated { .. } => { + self.request_workspace_config_refresh() + } } } @@ -4002,9 +4033,9 @@ impl LspStore { let local = this.as_local()?; let mut servers = Vec::new(); - for (seed, state) in &local.language_server_ids { - - let Some(states) = local.language_servers.get(&state.id) else { + for ((worktree_id, _), server_ids) in &local.language_server_ids { + for server_id in server_ids { + let Some(states) = local.language_servers.get(server_id) else { continue; }; let (json_adapter, json_server) = match states { @@ -4019,7 +4050,7 @@ impl LspStore { let Some(worktree) = this .worktree_store .read(cx) - .worktree_for_id(seed.worktree_id, cx) + .worktree_for_id(*worktree_id, cx) else { continue; }; @@ -4035,9 +4066,9 @@ impl LspStore { ); servers.push((json_adapter, json_server, json_delegate)); - + } } - Some(servers) + return Some(servers); }) .ok() .flatten(); @@ -4046,10 +4077,10 @@ impl LspStore { return; }; - let Ok(Some((fs, _))) = this.read_with(cx, |this, _| { + let Ok(Some((fs, toolchain_store))) = this.read_with(cx, |this, cx| { let local = this.as_local()?; - let toolchain_store = local.toolchain_store().clone(); - Some((local.fs.clone(), toolchain_store)) + let toolchain_store = this.toolchain_store(cx); + return Some((local.fs.clone(), toolchain_store)); }) else { return; }; @@ -4060,7 +4091,7 @@ impl LspStore { adapter, fs.as_ref(), &delegate, - None, + toolchain_store.clone(), cx, ) .await @@ -4129,7 +4160,7 @@ impl LspStore { local.registered_buffers.remove(&buffer_id); local.buffers_opened_in_servers.remove(&buffer_id); if let Some(file) = File::from_dyn(buffer.read(cx).file()).cloned() { - local.unregister_old_buffer_from_language_servers(buffer, &file, cx); + local.unregister_old_buffer_from_language_servers(&buffer, &file, cx); } } }) @@ -4199,12 +4230,14 @@ impl LspStore { if local .registered_buffers .contains_key(&buffer.read(cx).remote_id()) - && let Some(file_url) = - file_path_to_lsp_url(&f.abs_path(cx)).log_err() { - local.unregister_buffer_from_language_servers( - &buffer, &file_url, cx, - ); + if let Some(file_url) = + file_path_to_lsp_url(&f.abs_path(cx)).log_err() + { + local.unregister_buffer_from_language_servers( + &buffer, &file_url, cx, + ); + } } } } @@ -4302,19 +4335,25 @@ impl LspStore { let buffer = buffer_entity.read(cx); let buffer_file = buffer.file().cloned(); let buffer_id = buffer.remote_id(); - if let Some(local_store) = self.as_local_mut() - && local_store.registered_buffers.contains_key(&buffer_id) - && let Some(abs_path) = - File::from_dyn(buffer_file.as_ref()).map(|file| file.abs_path(cx)) - && let Some(file_url) = file_path_to_lsp_url(&abs_path).log_err() - { - local_store.unregister_buffer_from_language_servers(buffer_entity, &file_url, cx); + if let Some(local_store) = self.as_local_mut() { + if local_store.registered_buffers.contains_key(&buffer_id) { + if let Some(abs_path) = + File::from_dyn(buffer_file.as_ref()).map(|file| file.abs_path(cx)) + { + if let Some(file_url) = file_path_to_lsp_url(&abs_path).log_err() { + local_store.unregister_buffer_from_language_servers( + buffer_entity, + &file_url, + cx, + ); + } + } + } } buffer_entity.update(cx, |buffer, cx| { - if buffer - .language() - .is_none_or(|old_language| !Arc::ptr_eq(old_language, &new_language)) - { + if buffer.language().map_or(true, |old_language| { + !Arc::ptr_eq(old_language, &new_language) + }) { buffer.set_language(Some(new_language.clone()), cx); } }); @@ -4326,28 +4365,33 @@ impl LspStore { let worktree_id = if let Some(file) = buffer_file { let worktree = file.worktree.clone(); - if let Some(local) = self.as_local_mut() - && local.registered_buffers.contains_key(&buffer_id) - { - local.register_buffer_with_language_servers(buffer_entity, HashSet::default(), cx); + if let Some(local) = self.as_local_mut() { + if local.registered_buffers.contains_key(&buffer_id) { + local.register_buffer_with_language_servers( + buffer_entity, + HashSet::default(), + cx, + ); + } } Some(worktree.read(cx).id()) } else { None }; - if settings.prettier.allowed - && let Some(prettier_plugins) = prettier_store::prettier_plugins_for_language(&settings) - { - let prettier_store = self.as_local().map(|s| s.prettier_store.clone()); - if let Some(prettier_store) = prettier_store { - prettier_store.update(cx, |prettier_store, cx| { - prettier_store.install_default_prettier( - worktree_id, - prettier_plugins.iter().map(|s| Arc::from(s.as_str())), - cx, - ) - }) + if settings.prettier.allowed { + if let Some(prettier_plugins) = prettier_store::prettier_plugins_for_language(&settings) + { + let prettier_store = self.as_local().map(|s| s.prettier_store.clone()); + if let Some(prettier_store) = prettier_store { + prettier_store.update(cx, |prettier_store, cx| { + prettier_store.install_default_prettier( + worktree_id, + prettier_plugins.iter().map(|s| Arc::from(s.as_str())), + cx, + ) + }) + } } } @@ -4366,92 +4410,37 @@ impl LspStore { } pub(crate) fn send_diagnostic_summaries(&self, worktree: &mut Worktree) { - if let Some((client, downstream_project_id)) = self.downstream_client.clone() - && let Some(diangostic_summaries) = self.diagnostic_summaries.get(&worktree.id()) - { - let mut summaries = diangostic_summaries.iter().flat_map(|(path, summaries)| { - summaries - .iter() - .map(|(server_id, summary)| summary.to_proto(*server_id, path)) - }); - if let Some(summary) = summaries.next() { - client - .send(proto::UpdateDiagnosticSummary { - project_id: downstream_project_id, - worktree_id: worktree.id().to_proto(), - summary: Some(summary), - more_summaries: summaries.collect(), - }) - .log_err(); + if let Some((client, downstream_project_id)) = self.downstream_client.clone() { + if let Some(summaries) = self.diagnostic_summaries.get(&worktree.id()) { + for (path, summaries) in summaries { + for (&server_id, summary) in summaries { + client + .send(proto::UpdateDiagnosticSummary { + project_id: downstream_project_id, + worktree_id: worktree.id().to_proto(), + summary: Some(summary.to_proto(server_id, path)), + }) + .log_err(); + } + } } } } - fn is_capable_for_proto_request( - &self, - buffer: &Entity, - request: &R, - cx: &Context, - ) -> bool - where - R: LspCommand, - { - self.check_if_capable_for_proto_request( - buffer, - |capabilities| { - request.check_capabilities(AdapterServerCapabilities { - server_capabilities: capabilities.clone(), - code_action_kinds: None, - }) - }, - cx, - ) - } - - fn check_if_capable_for_proto_request( - &self, - buffer: &Entity, - check: F, - cx: &Context, - ) -> bool - where - F: Fn(&lsp::ServerCapabilities) -> bool, - { - let Some(language) = buffer.read(cx).language().cloned() else { - return false; - }; - let relevant_language_servers = self - .languages - .lsp_adapters(&language.name()) - .into_iter() - .map(|lsp_adapter| lsp_adapter.name()) - .collect::>(); - self.language_server_statuses - .iter() - .filter_map(|(server_id, server_status)| { - relevant_language_servers - .contains(&server_status.name) - .then_some(server_id) - }) - .filter_map(|server_id| self.lsp_server_capabilities.get(server_id)) - .any(check) - } - - pub fn request_lsp( + pub fn request_lsp( &mut self, - buffer: Entity, + buffer_handle: Entity, server: LanguageServerToQuery, request: R, cx: &mut Context, ) -> Task> where - R: LspCommand, ::Result: Send, ::Params: Send, { if let Some((upstream_client, upstream_project_id)) = self.upstream_client() { return self.send_lsp_proto_request( - buffer, + buffer_handle, upstream_client, upstream_project_id, request, @@ -4459,7 +4448,7 @@ impl LspStore { ); } - let Some(language_server) = buffer.update(cx, |buffer, cx| match server { + let Some(language_server) = buffer_handle.update(cx, |buffer, cx| match server { LanguageServerToQuery::FirstCapable => self.as_local().and_then(|local| { local .language_servers_for_buffer(buffer, cx) @@ -4479,7 +4468,8 @@ impl LspStore { return Task::ready(Ok(Default::default())); }; - let file = File::from_dyn(buffer.read(cx).file()).and_then(File::as_local); + let buffer = buffer_handle.read(cx); + let file = File::from_dyn(buffer.file()).and_then(File::as_local); let Some(file) = file else { return Task::ready(Ok(Default::default())); @@ -4487,7 +4477,7 @@ impl LspStore { let lsp_params = match request.to_lsp_params_or_response( &file.abs_path(cx), - buffer.read(cx), + buffer, &language_server, cx, ) { @@ -4510,7 +4500,7 @@ impl LspStore { if !request.check_capabilities(language_server.adapter_server_capabilities()) { return Task::ready(Ok(Default::default())); } - cx.spawn(async move |this, cx| { + return cx.spawn(async move |this, cx| { let lsp_request = language_server.request::(lsp_params); let id = lsp_request.id(); @@ -4559,16 +4549,17 @@ impl LspStore { anyhow::anyhow!(message) })?; - request + let response = request .response_from_lsp( response, this.upgrade().context("no app context")?, - buffer, + buffer_handle, language_server.server_id(), cx.clone(), ) - .await - }) + .await; + response + }); } fn on_settings_changed(&mut self, cx: &mut Context) { @@ -4586,7 +4577,7 @@ impl LspStore { } } - self.request_workspace_config_refresh(); + self.refresh_server_tree(cx); if let Some(prettier_store) = self.as_local().map(|s| s.prettier_store.clone()) { prettier_store.update(cx, |prettier_store, cx| { @@ -4599,148 +4590,156 @@ impl LspStore { fn refresh_server_tree(&mut self, cx: &mut Context) { let buffer_store = self.buffer_store.clone(); - let Some(local) = self.as_local_mut() else { - return; - }; - let mut adapters = BTreeMap::default(); - let get_adapter = { - let languages = local.languages.clone(); - let environment = local.environment.clone(); - let weak = local.weak.clone(); - let worktree_store = local.worktree_store.clone(); - let http_client = local.http_client.clone(); - let fs = local.fs.clone(); - move |worktree_id, cx: &mut App| { - let worktree = worktree_store.read(cx).worktree_for_id(worktree_id, cx)?; - Some(LocalLspAdapterDelegate::new( - languages.clone(), - &environment, - weak.clone(), - &worktree, - http_client.clone(), - fs.clone(), - cx, - )) - } - }; + if let Some(local) = self.as_local_mut() { + let mut adapters = BTreeMap::default(); + let get_adapter = { + let languages = local.languages.clone(); + let environment = local.environment.clone(); + let weak = local.weak.clone(); + let worktree_store = local.worktree_store.clone(); + let http_client = local.http_client.clone(); + let fs = local.fs.clone(); + move |worktree_id, cx: &mut App| { + let worktree = worktree_store.read(cx).worktree_for_id(worktree_id, cx)?; + Some(LocalLspAdapterDelegate::new( + languages.clone(), + &environment, + weak.clone(), + &worktree, + http_client.clone(), + fs.clone(), + cx, + )) + } + }; - let mut messages_to_report = Vec::new(); - let (new_tree, to_stop) = { - let mut rebase = local.lsp_tree.rebase(); - let buffers = buffer_store - .read(cx) - .buffers() - .filter_map(|buffer| { - let raw_buffer = buffer.read(cx); - if !local - .registered_buffers - .contains_key(&raw_buffer.remote_id()) - { - return None; + let mut messages_to_report = Vec::new(); + let to_stop = local.lsp_tree.clone().update(cx, |lsp_tree, cx| { + let mut rebase = lsp_tree.rebase(); + for buffer_handle in buffer_store.read(cx).buffers().sorted_by_key(|buffer| { + Reverse( + File::from_dyn(buffer.read(cx).file()) + .map(|file| file.worktree.read(cx).is_visible()), + ) + }) { + let buffer = buffer_handle.read(cx); + if !local.registered_buffers.contains_key(&buffer.remote_id()) { + continue; } - let file = File::from_dyn(raw_buffer.file()).cloned()?; - let language = raw_buffer.language().cloned()?; - Some((file, language, raw_buffer.remote_id())) - }) - .sorted_by_key(|(file, _, _)| Reverse(file.worktree.read(cx).is_visible())); - for (file, language, buffer_id) in buffers { - let worktree_id = file.worktree_id(cx); - let Some(worktree) = local - .worktree_store - .read(cx) - .worktree_for_id(worktree_id, cx) - else { - continue; - }; + if let Some((file, language)) = File::from_dyn(buffer.file()) + .cloned() + .zip(buffer.language().map(|l| l.name())) + { + let worktree_id = file.worktree_id(cx); + let Some(worktree) = local + .worktree_store + .read(cx) + .worktree_for_id(worktree_id, cx) + else { + continue; + }; - if let Some((_, apply)) = local.reuse_existing_language_server( - rebase.server_tree(), - &worktree, - &language.name(), - cx, - ) { - (apply)(rebase.server_tree()); - } else if let Some(lsp_delegate) = adapters - .entry(worktree_id) - .or_insert_with(|| get_adapter(worktree_id, cx)) - .clone() - { - let delegate = - Arc::new(ManifestQueryDelegate::new(worktree.read(cx).snapshot())); - let path = file - .path() - .parent() - .map(Arc::from) - .unwrap_or_else(|| file.path().clone()); - let worktree_path = ProjectPath { worktree_id, path }; - let abs_path = file.abs_path(cx); - let worktree_root = worktree.read(cx).abs_path(); - let nodes = rebase - .walk( - worktree_path, - language.name(), - language.manifest(), - delegate.clone(), - cx, - ) - .collect::>(); - for node in nodes { - let server_id = node.server_id_or_init(|disposition| { - let path = &disposition.path; - let uri = Url::from_file_path(worktree_root.join(&path.path)); - let key = LanguageServerSeed { - worktree_id, - name: disposition.server_name.clone(), - settings: disposition.settings.clone(), - toolchain: local.toolchain_store.read(cx).active_toolchain( - path.worktree_id, - &path.path, - language.name(), - ), - }; - local.language_server_ids.remove(&key); - - let server_id = local.get_or_insert_language_server( + let Some((reused, delegate, nodes)) = local + .reuse_existing_language_server( + rebase.server_tree(), &worktree, - lsp_delegate.clone(), - disposition, - &language.name(), + &language, cx, - ); - if let Some(state) = local.language_servers.get(&server_id) - && let Ok(uri) = uri - { - state.add_workspace_folder(uri); - }; - server_id - }); + ) + .map(|(delegate, servers)| (true, delegate, servers)) + .or_else(|| { + let lsp_delegate = adapters + .entry(worktree_id) + .or_insert_with(|| get_adapter(worktree_id, cx)) + .clone()?; + let delegate = Arc::new(ManifestQueryDelegate::new( + worktree.read(cx).snapshot(), + )); + let path = file + .path() + .parent() + .map(Arc::from) + .unwrap_or_else(|| file.path().clone()); + let worktree_path = ProjectPath { worktree_id, path }; - if let Some(language_server_id) = server_id { - messages_to_report.push(LspStoreEvent::LanguageServerUpdate { - language_server_id, - name: node.name(), - message: - proto::update_language_server::Variant::RegisteredForBuffer( - proto::RegisteredForBuffer { - buffer_abs_path: abs_path.to_string_lossy().to_string(), - buffer_id: buffer_id.to_proto(), - }, - ), - }); + let nodes = rebase.get( + worktree_path, + AdapterQuery::Language(&language), + delegate.clone(), + cx, + ); + + Some((false, lsp_delegate, nodes.collect())) + }) + else { + continue; + }; + + let abs_path = file.abs_path(cx); + for node in nodes { + if !reused { + let server_id = node.server_id_or_init( + |LaunchDisposition { + server_name, + + path, + settings, + }| + { + let uri = Url::from_file_path( + worktree.read(cx).abs_path().join(&path.path), + ); + let key = (worktree_id, server_name.clone()); + local.language_server_ids.remove(&key); + + let adapter = local + .languages + .lsp_adapters(&language) + .into_iter() + .find(|adapter| &adapter.name() == server_name) + .expect("To find LSP adapter"); + let server_id = local.start_language_server( + &worktree, + delegate.clone(), + adapter, + settings, + cx, + ); + if let Some(state) = + local.language_servers.get(&server_id) + { + if let Ok(uri) = uri { + state.add_workspace_folder(uri); + }; + } + server_id + } + ); + + if let Some(language_server_id) = server_id { + messages_to_report.push(LspStoreEvent::LanguageServerUpdate { + language_server_id, + name: node.name(), + message: + proto::update_language_server::Variant::RegisteredForBuffer( + proto::RegisteredForBuffer { + buffer_abs_path: abs_path.to_string_lossy().to_string(), + }, + ), + }); + } + } } } - } else { - continue; } + rebase.finish() + }); + for message in messages_to_report { + cx.emit(message); + } + for (id, _) in to_stop { + self.stop_local_language_server(id, cx).detach(); } - rebase.finish() - }; - for message in messages_to_report { - cx.emit(message); - } - local.lsp_tree = new_tree; - for (id, _) in to_stop { - self.stop_local_language_server(id, cx).detach(); } } @@ -4772,7 +4771,7 @@ impl LspStore { .await }) } else if self.mode.is_local() { - let Some((_, lang_server)) = buffer_handle.update(cx, |buffer, cx| { + let Some((lsp_adapter, lang_server)) = buffer_handle.update(cx, |buffer, cx| { self.language_server_for_local_buffer(buffer, action.server_id, cx) .map(|(adapter, server)| (adapter.clone(), server.clone())) }) else { @@ -4782,18 +4781,19 @@ impl LspStore { LocalLspStore::try_resolve_code_action(&lang_server, &mut action) .await .context("resolving a code action")?; - if let Some(edit) = action.lsp_action.edit() - && (edit.changes.is_some() || edit.document_changes.is_some()) { + if let Some(edit) = action.lsp_action.edit() { + if edit.changes.is_some() || edit.document_changes.is_some() { return LocalLspStore::deserialize_workspace_edit( this.upgrade().context("no app present")?, edit.clone(), push_to_history, - + lsp_adapter.clone(), lang_server.clone(), cx, ) .await; } + } if let Some(command) = action.lsp_action.command() { let server_capabilities = lang_server.capabilities(); @@ -4845,7 +4845,7 @@ impl LspStore { push_to_history: bool, cx: &mut Context, ) -> Task> { - if self.as_local().is_some() { + if let Some(_) = self.as_local() { cx.spawn(async move |lsp_store, cx| { let buffers = buffers.into_iter().collect::>(); let result = LocalLspStore::execute_code_action_kind_locally( @@ -4899,20 +4899,15 @@ impl LspStore { pub fn resolve_inlay_hint( &self, - mut hint: InlayHint, - buffer: Entity, + hint: InlayHint, + buffer_handle: Entity, server_id: LanguageServerId, cx: &mut Context, ) -> Task> { if let Some((upstream_client, project_id)) = self.upstream_client() { - if !self.check_if_capable_for_proto_request(&buffer, InlayHints::can_resolve_inlays, cx) - { - hint.resolve_state = ResolveState::Resolved; - return Task::ready(Ok(hint)); - } let request = proto::ResolveInlayHint { project_id, - buffer_id: buffer.read(cx).remote_id().into(), + buffer_id: buffer_handle.read(cx).remote_id().into(), language_server_id: server_id.0 as u64, hint: Some(InlayHints::project_to_proto_hint(hint.clone())), }; @@ -4928,7 +4923,7 @@ impl LspStore { } }) } else { - let Some(lang_server) = buffer.update(cx, |buffer, cx| { + let Some(lang_server) = buffer_handle.update(cx, |buffer, cx| { self.language_server_for_local_buffer(buffer, server_id, cx) .map(|(_, server)| server.clone()) }) else { @@ -4937,7 +4932,7 @@ impl LspStore { if !InlayHints::can_resolve_inlays(&lang_server.capabilities()) { return Task::ready(Ok(hint)); } - let buffer_snapshot = buffer.read(cx).snapshot(); + let buffer_snapshot = buffer_handle.read(cx).snapshot(); cx.spawn(async move |_, cx| { let resolve_task = lang_server.request::( InlayHints::project_to_lsp_hint(hint, &buffer_snapshot), @@ -4948,7 +4943,7 @@ impl LspStore { .context("inlay hint resolve LSP request")?; let resolved_hint = InlayHints::lsp_to_project_hint( resolved_hint, - &buffer, + &buffer_handle, server_id, ResolveState::Resolved, false, @@ -5059,7 +5054,7 @@ impl LspStore { } } - pub(crate) fn linked_edits( + pub(crate) fn linked_edit( &mut self, buffer: &Entity, position: Anchor, @@ -5074,7 +5069,10 @@ impl LspStore { local .language_servers_for_buffer(buffer, cx) .filter(|(_, server)| { - LinkedEditingRange::check_server_capabilities(server.capabilities()) + server + .capabilities() + .linked_editing_range_provider + .is_some() }) .filter(|(adapter, _)| { scope @@ -5101,7 +5099,7 @@ impl LspStore { }) == Some(true) }) else { - return Task::ready(Ok(Vec::new())); + return Task::ready(Ok(vec![])); }; self.request_lsp( @@ -5120,15 +5118,6 @@ impl LspStore { cx: &mut Context, ) -> Task>> { if let Some((client, project_id)) = self.upstream_client() { - if !self.check_if_capable_for_proto_request( - &buffer, - |capabilities| { - OnTypeFormatting::supports_on_type_formatting(&trigger, capabilities) - }, - cx, - ) { - return Task::ready(Ok(None)); - } let request = proto::OnTypeFormatting { project_id, buffer_id: buffer.read(cx).remote_id().into(), @@ -5235,395 +5224,448 @@ impl LspStore { pub fn definitions( &mut self, - buffer: &Entity, + buffer_handle: &Entity, position: PointUtf16, cx: &mut Context, - ) -> Task>>> { + ) -> Task>> { if let Some((upstream_client, project_id)) = self.upstream_client() { - let request = GetDefinitions { position }; - if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Ok(None)); - } - let request_task = upstream_client.request_lsp( + let request_task = upstream_client.request(proto::MultiLspQuery { + buffer_id: buffer_handle.read(cx).remote_id().into(), + version: serialize_version(&buffer_handle.read(cx).version()), project_id, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.to_proto(project_id, buffer.read(cx)), - ); - let buffer = buffer.clone(); + strategy: Some(proto::multi_lsp_query::Strategy::All( + proto::AllLanguageServers {}, + )), + request: Some(proto::multi_lsp_query::Request::GetDefinition( + GetDefinitions { position }.to_proto(project_id, buffer_handle.read(cx)), + )), + }); + let buffer = buffer_handle.clone(); cx.spawn(async move |weak_project, cx| { let Some(project) = weak_project.upgrade() else { - return Ok(None); + return Ok(Vec::new()); }; - let Some(responses) = request_task.await? else { - return Ok(None); - }; - let actions = join_all(responses.payload.into_iter().map(|response| { - GetDefinitions { position }.response_from_proto( - response.response, - project.clone(), - buffer.clone(), - cx.clone(), - ) - })) + let responses = request_task.await?.responses; + let actions = join_all( + responses + .into_iter() + .filter_map(|lsp_response| match lsp_response.response? { + proto::lsp_response::Response::GetDefinitionResponse(response) => { + Some(response) + } + unexpected => { + debug_panic!("Unexpected response: {unexpected:?}"); + None + } + }) + .map(|definitions_response| { + GetDefinitions { position }.response_from_proto( + definitions_response, + project.clone(), + buffer.clone(), + cx.clone(), + ) + }), + ) .await; - Ok(Some( - actions - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .dedup() - .collect(), - )) + Ok(actions + .into_iter() + .collect::>>>()? + .into_iter() + .flatten() + .dedup() + .collect()) }) } else { let definitions_task = self.request_multiple_lsp_locally( - buffer, + buffer_handle, Some(position), GetDefinitions { position }, cx, ); cx.background_spawn(async move { - Ok(Some( - definitions_task - .await - .into_iter() - .flat_map(|(_, definitions)| definitions) - .dedup() - .collect(), - )) + Ok(definitions_task + .await + .into_iter() + .flat_map(|(_, definitions)| definitions) + .dedup() + .collect()) }) } } pub fn declarations( &mut self, - buffer: &Entity, + buffer_handle: &Entity, position: PointUtf16, cx: &mut Context, - ) -> Task>>> { + ) -> Task>> { if let Some((upstream_client, project_id)) = self.upstream_client() { - let request = GetDeclarations { position }; - if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Ok(None)); - } - let request_task = upstream_client.request_lsp( + let request_task = upstream_client.request(proto::MultiLspQuery { + buffer_id: buffer_handle.read(cx).remote_id().into(), + version: serialize_version(&buffer_handle.read(cx).version()), project_id, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.to_proto(project_id, buffer.read(cx)), - ); - let buffer = buffer.clone(); + strategy: Some(proto::multi_lsp_query::Strategy::All( + proto::AllLanguageServers {}, + )), + request: Some(proto::multi_lsp_query::Request::GetDeclaration( + GetDeclarations { position }.to_proto(project_id, buffer_handle.read(cx)), + )), + }); + let buffer = buffer_handle.clone(); cx.spawn(async move |weak_project, cx| { let Some(project) = weak_project.upgrade() else { - return Ok(None); + return Ok(Vec::new()); }; - let Some(responses) = request_task.await? else { - return Ok(None); - }; - let actions = join_all(responses.payload.into_iter().map(|response| { - GetDeclarations { position }.response_from_proto( - response.response, - project.clone(), - buffer.clone(), - cx.clone(), - ) - })) + let responses = request_task.await?.responses; + let actions = join_all( + responses + .into_iter() + .filter_map(|lsp_response| match lsp_response.response? { + proto::lsp_response::Response::GetDeclarationResponse(response) => { + Some(response) + } + unexpected => { + debug_panic!("Unexpected response: {unexpected:?}"); + None + } + }) + .map(|declarations_response| { + GetDeclarations { position }.response_from_proto( + declarations_response, + project.clone(), + buffer.clone(), + cx.clone(), + ) + }), + ) .await; - Ok(Some( - actions - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .dedup() - .collect(), - )) + Ok(actions + .into_iter() + .collect::>>>()? + .into_iter() + .flatten() + .dedup() + .collect()) }) } else { let declarations_task = self.request_multiple_lsp_locally( - buffer, + buffer_handle, Some(position), GetDeclarations { position }, cx, ); cx.background_spawn(async move { - Ok(Some( - declarations_task - .await - .into_iter() - .flat_map(|(_, declarations)| declarations) - .dedup() - .collect(), - )) + Ok(declarations_task + .await + .into_iter() + .flat_map(|(_, declarations)| declarations) + .dedup() + .collect()) }) } } pub fn type_definitions( &mut self, - buffer: &Entity, + buffer_handle: &Entity, position: PointUtf16, cx: &mut Context, - ) -> Task>>> { + ) -> Task>> { if let Some((upstream_client, project_id)) = self.upstream_client() { - let request = GetTypeDefinitions { position }; - if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Ok(None)); - } - let request_task = upstream_client.request_lsp( + let request_task = upstream_client.request(proto::MultiLspQuery { + buffer_id: buffer_handle.read(cx).remote_id().into(), + version: serialize_version(&buffer_handle.read(cx).version()), project_id, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.to_proto(project_id, buffer.read(cx)), - ); - let buffer = buffer.clone(); + strategy: Some(proto::multi_lsp_query::Strategy::All( + proto::AllLanguageServers {}, + )), + request: Some(proto::multi_lsp_query::Request::GetTypeDefinition( + GetTypeDefinitions { position }.to_proto(project_id, buffer_handle.read(cx)), + )), + }); + let buffer = buffer_handle.clone(); cx.spawn(async move |weak_project, cx| { let Some(project) = weak_project.upgrade() else { - return Ok(None); + return Ok(Vec::new()); }; - let Some(responses) = request_task.await? else { - return Ok(None); - }; - let actions = join_all(responses.payload.into_iter().map(|response| { - GetTypeDefinitions { position }.response_from_proto( - response.response, - project.clone(), - buffer.clone(), - cx.clone(), - ) - })) + let responses = request_task.await?.responses; + let actions = join_all( + responses + .into_iter() + .filter_map(|lsp_response| match lsp_response.response? { + proto::lsp_response::Response::GetTypeDefinitionResponse(response) => { + Some(response) + } + unexpected => { + debug_panic!("Unexpected response: {unexpected:?}"); + None + } + }) + .map(|type_definitions_response| { + GetTypeDefinitions { position }.response_from_proto( + type_definitions_response, + project.clone(), + buffer.clone(), + cx.clone(), + ) + }), + ) .await; - Ok(Some( - actions - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .dedup() - .collect(), - )) + Ok(actions + .into_iter() + .collect::>>>()? + .into_iter() + .flatten() + .dedup() + .collect()) }) } else { let type_definitions_task = self.request_multiple_lsp_locally( - buffer, + buffer_handle, Some(position), GetTypeDefinitions { position }, cx, ); cx.background_spawn(async move { - Ok(Some( - type_definitions_task - .await - .into_iter() - .flat_map(|(_, type_definitions)| type_definitions) - .dedup() - .collect(), - )) + Ok(type_definitions_task + .await + .into_iter() + .flat_map(|(_, type_definitions)| type_definitions) + .dedup() + .collect()) }) } } pub fn implementations( &mut self, - buffer: &Entity, + buffer_handle: &Entity, position: PointUtf16, cx: &mut Context, - ) -> Task>>> { + ) -> Task>> { if let Some((upstream_client, project_id)) = self.upstream_client() { - let request = GetImplementations { position }; - if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Ok(None)); - } - let request_task = upstream_client.request_lsp( + let request_task = upstream_client.request(proto::MultiLspQuery { + buffer_id: buffer_handle.read(cx).remote_id().into(), + version: serialize_version(&buffer_handle.read(cx).version()), project_id, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.to_proto(project_id, buffer.read(cx)), - ); - let buffer = buffer.clone(); + strategy: Some(proto::multi_lsp_query::Strategy::All( + proto::AllLanguageServers {}, + )), + request: Some(proto::multi_lsp_query::Request::GetImplementation( + GetImplementations { position }.to_proto(project_id, buffer_handle.read(cx)), + )), + }); + let buffer = buffer_handle.clone(); cx.spawn(async move |weak_project, cx| { let Some(project) = weak_project.upgrade() else { - return Ok(None); + return Ok(Vec::new()); }; - let Some(responses) = request_task.await? else { - return Ok(None); - }; - let actions = join_all(responses.payload.into_iter().map(|response| { - GetImplementations { position }.response_from_proto( - response.response, - project.clone(), - buffer.clone(), - cx.clone(), - ) - })) + let responses = request_task.await?.responses; + let actions = join_all( + responses + .into_iter() + .filter_map(|lsp_response| match lsp_response.response? { + proto::lsp_response::Response::GetImplementationResponse(response) => { + Some(response) + } + unexpected => { + debug_panic!("Unexpected response: {unexpected:?}"); + None + } + }) + .map(|implementations_response| { + GetImplementations { position }.response_from_proto( + implementations_response, + project.clone(), + buffer.clone(), + cx.clone(), + ) + }), + ) .await; - Ok(Some( - actions - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .dedup() - .collect(), - )) + Ok(actions + .into_iter() + .collect::>>>()? + .into_iter() + .flatten() + .dedup() + .collect()) }) } else { let implementations_task = self.request_multiple_lsp_locally( - buffer, + buffer_handle, Some(position), GetImplementations { position }, cx, ); cx.background_spawn(async move { - Ok(Some( - implementations_task - .await - .into_iter() - .flat_map(|(_, implementations)| implementations) - .dedup() - .collect(), - )) + Ok(implementations_task + .await + .into_iter() + .flat_map(|(_, implementations)| implementations) + .dedup() + .collect()) }) } } pub fn references( &mut self, - buffer: &Entity, + buffer_handle: &Entity, position: PointUtf16, cx: &mut Context, - ) -> Task>>> { + ) -> Task>> { if let Some((upstream_client, project_id)) = self.upstream_client() { - let request = GetReferences { position }; - if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Ok(None)); - } - - let request_task = upstream_client.request_lsp( + let request_task = upstream_client.request(proto::MultiLspQuery { + buffer_id: buffer_handle.read(cx).remote_id().into(), + version: serialize_version(&buffer_handle.read(cx).version()), project_id, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.to_proto(project_id, buffer.read(cx)), - ); - let buffer = buffer.clone(); + strategy: Some(proto::multi_lsp_query::Strategy::All( + proto::AllLanguageServers {}, + )), + request: Some(proto::multi_lsp_query::Request::GetReferences( + GetReferences { position }.to_proto(project_id, buffer_handle.read(cx)), + )), + }); + let buffer = buffer_handle.clone(); cx.spawn(async move |weak_project, cx| { let Some(project) = weak_project.upgrade() else { - return Ok(None); - }; - let Some(responses) = request_task.await? else { - return Ok(None); + return Ok(Vec::new()); }; + let responses = request_task.await?.responses; + let actions = join_all( + responses + .into_iter() + .filter_map(|lsp_response| match lsp_response.response? { + proto::lsp_response::Response::GetReferencesResponse(response) => { + Some(response) + } + unexpected => { + debug_panic!("Unexpected response: {unexpected:?}"); + None + } + }) + .map(|references_response| { + GetReferences { position }.response_from_proto( + references_response, + project.clone(), + buffer.clone(), + cx.clone(), + ) + }), + ) + .await; - let locations = join_all(responses.payload.into_iter().map(|lsp_response| { - GetReferences { position }.response_from_proto( - lsp_response.response, - project.clone(), - buffer.clone(), - cx.clone(), - ) - })) - .await - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .dedup() - .collect(); - Ok(Some(locations)) + Ok(actions + .into_iter() + .collect::>>>()? + .into_iter() + .flatten() + .dedup() + .collect()) }) } else { let references_task = self.request_multiple_lsp_locally( - buffer, + buffer_handle, Some(position), GetReferences { position }, cx, ); cx.background_spawn(async move { - Ok(Some( - references_task - .await - .into_iter() - .flat_map(|(_, references)| references) - .dedup() - .collect(), - )) + Ok(references_task + .await + .into_iter() + .flat_map(|(_, references)| references) + .dedup() + .collect()) }) } } pub fn code_actions( &mut self, - buffer: &Entity, + buffer_handle: &Entity, range: Range, kinds: Option>, cx: &mut Context, - ) -> Task>>> { + ) -> Task>> { if let Some((upstream_client, project_id)) = self.upstream_client() { - let request = GetCodeActions { - range: range.clone(), - kinds: kinds.clone(), - }; - if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Ok(None)); - } - let request_task = upstream_client.request_lsp( + let request_task = upstream_client.request(proto::MultiLspQuery { + buffer_id: buffer_handle.read(cx).remote_id().into(), + version: serialize_version(&buffer_handle.read(cx).version()), project_id, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.to_proto(project_id, buffer.read(cx)), - ); - let buffer = buffer.clone(); - cx.spawn(async move |weak_project, cx| { - let Some(project) = weak_project.upgrade() else { - return Ok(None); - }; - let Some(responses) = request_task.await? else { - return Ok(None); - }; - let actions = join_all(responses.payload.into_iter().map(|response| { + strategy: Some(proto::multi_lsp_query::Strategy::All( + proto::AllLanguageServers {}, + )), + request: Some(proto::multi_lsp_query::Request::GetCodeActions( GetCodeActions { range: range.clone(), kinds: kinds.clone(), } - .response_from_proto( - response.response, - project.clone(), - buffer.clone(), - cx.clone(), - ) - })) + .to_proto(project_id, buffer_handle.read(cx)), + )), + }); + let buffer = buffer_handle.clone(); + cx.spawn(async move |weak_project, cx| { + let Some(project) = weak_project.upgrade() else { + return Ok(Vec::new()); + }; + let responses = request_task.await?.responses; + let actions = join_all( + responses + .into_iter() + .filter_map(|lsp_response| match lsp_response.response? { + proto::lsp_response::Response::GetCodeActionsResponse(response) => { + Some(response) + } + unexpected => { + debug_panic!("Unexpected response: {unexpected:?}"); + None + } + }) + .map(|code_actions_response| { + GetCodeActions { + range: range.clone(), + kinds: kinds.clone(), + } + .response_from_proto( + code_actions_response, + project.clone(), + buffer.clone(), + cx.clone(), + ) + }), + ) .await; - Ok(Some( - actions - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .collect(), - )) + Ok(actions + .into_iter() + .collect::>>>()? + .into_iter() + .flatten() + .collect()) }) } else { let all_actions_task = self.request_multiple_lsp_locally( - buffer, + buffer_handle, Some(range.start), - GetCodeActions { range, kinds }, + GetCodeActions { + range: range.clone(), + kinds: kinds.clone(), + }, cx, ); cx.background_spawn(async move { - Ok(Some( - all_actions_task - .await - .into_iter() - .flat_map(|(_, actions)| actions) - .collect(), - )) + Ok(all_actions_task + .await + .into_iter() + .flat_map(|(_, actions)| actions) + .collect()) }) } } @@ -5636,30 +5678,28 @@ impl LspStore { let version_queried_for = buffer.read(cx).version(); let buffer_id = buffer.read(cx).remote_id(); - if let Some(cached_data) = self.lsp_code_lens.get(&buffer_id) - && !version_queried_for.changed_since(&cached_data.lens_for_version) - { - let has_different_servers = self.as_local().is_some_and(|local| { - local - .buffers_opened_in_servers - .get(&buffer_id) - .cloned() - .unwrap_or_default() - != cached_data.lens.keys().copied().collect() - }); - if !has_different_servers { - return Task::ready(Ok(Some( - cached_data.lens.values().flatten().cloned().collect(), - ))) - .shared(); + if let Some(cached_data) = self.lsp_code_lens.get(&buffer_id) { + if !version_queried_for.changed_since(&cached_data.lens_for_version) { + let has_different_servers = self.as_local().is_some_and(|local| { + local + .buffers_opened_in_servers + .get(&buffer_id) + .cloned() + .unwrap_or_default() + != cached_data.lens.keys().copied().collect() + }); + if !has_different_servers { + return Task::ready(Ok(cached_data.lens.values().flatten().cloned().collect())) + .shared(); + } } } let lsp_data = self.lsp_code_lens.entry(buffer_id).or_default(); - if let Some((updating_for, running_update)) = &lsp_data.update - && !version_queried_for.changed_since(updating_for) - { - return running_update.clone(); + if let Some((updating_for, running_update)) = &lsp_data.update { + if !version_queried_for.changed_since(&updating_for) { + return running_update.clone(); + } } let buffer = buffer.clone(); let query_version_queried_for = version_queried_for.clone(); @@ -5689,19 +5729,17 @@ impl LspStore { lsp_store .update(cx, |lsp_store, _| { let lsp_data = lsp_store.lsp_code_lens.entry(buffer_id).or_default(); - if let Some(fetched_lens) = fetched_lens { - if lsp_data.lens_for_version == query_version_queried_for { - lsp_data.lens.extend(fetched_lens); - } else if !lsp_data - .lens_for_version - .changed_since(&query_version_queried_for) - { - lsp_data.lens_for_version = query_version_queried_for; - lsp_data.lens = fetched_lens; - } + if lsp_data.lens_for_version == query_version_queried_for { + lsp_data.lens.extend(fetched_lens.clone()); + } else if !lsp_data + .lens_for_version + .changed_since(&query_version_queried_for) + { + lsp_data.lens_for_version = query_version_queried_for; + lsp_data.lens = fetched_lens.clone(); } lsp_data.update = None; - Some(lsp_data.lens.values().flatten().cloned().collect()) + lsp_data.lens.values().flatten().cloned().collect() }) .map_err(Arc::new) }) @@ -5714,40 +5752,60 @@ impl LspStore { &mut self, buffer: &Entity, cx: &mut Context, - ) -> Task>>>> { + ) -> Task>>> { if let Some((upstream_client, project_id)) = self.upstream_client() { - let request = GetCodeLens; - if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Ok(None)); - } - let request_task = upstream_client.request_lsp( + let request_task = upstream_client.request(proto::MultiLspQuery { + buffer_id: buffer.read(cx).remote_id().into(), + version: serialize_version(&buffer.read(cx).version()), project_id, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.to_proto(project_id, buffer.read(cx)), - ); + strategy: Some(proto::multi_lsp_query::Strategy::All( + proto::AllLanguageServers {}, + )), + request: Some(proto::multi_lsp_query::Request::GetCodeLens( + GetCodeLens.to_proto(project_id, buffer.read(cx)), + )), + }); let buffer = buffer.clone(); cx.spawn(async move |weak_lsp_store, cx| { let Some(lsp_store) = weak_lsp_store.upgrade() else { - return Ok(None); + return Ok(HashMap::default()); }; - let Some(responses) = request_task.await? else { - return Ok(None); - }; - - let code_lens_actions = join_all(responses.payload.into_iter().map(|response| { - let lsp_store = lsp_store.clone(); - let buffer = buffer.clone(); - let cx = cx.clone(); - async move { - ( - LanguageServerId::from_proto(response.server_id), - GetCodeLens - .response_from_proto(response.response, lsp_store, buffer, cx) - .await, - ) - } - })) + let responses = request_task.await?.responses; + let code_lens_actions = join_all( + responses + .into_iter() + .filter_map(|lsp_response| { + let response = match lsp_response.response? { + proto::lsp_response::Response::GetCodeLensResponse(response) => { + Some(response) + } + unexpected => { + debug_panic!("Unexpected response: {unexpected:?}"); + None + } + }?; + let server_id = LanguageServerId::from_proto(lsp_response.server_id); + Some((server_id, response)) + }) + .map(|(server_id, code_lens_response)| { + let lsp_store = lsp_store.clone(); + let buffer = buffer.clone(); + let cx = cx.clone(); + async move { + ( + server_id, + GetCodeLens + .response_from_proto( + code_lens_response, + lsp_store, + buffer, + cx, + ) + .await, + ) + } + }), + ) .await; let mut has_errors = false; @@ -5766,14 +5824,14 @@ impl LspStore { !has_errors || !code_lens_actions.is_empty(), "Failed to fetch code lens" ); - Ok(Some(code_lens_actions)) + Ok(code_lens_actions) }) } else { let code_lens_actions_task = self.request_multiple_lsp_locally(buffer, None::, GetCodeLens, cx); - cx.background_spawn(async move { - Ok(Some(code_lens_actions_task.await.into_iter().collect())) - }) + cx.background_spawn( + async move { Ok(code_lens_actions_task.await.into_iter().collect()) }, + ) } } @@ -5788,15 +5846,11 @@ impl LspStore { let language_registry = self.languages.clone(); if let Some((upstream_client, project_id)) = self.upstream_client() { - let request = GetCompletions { position, context }; - if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Ok(Vec::new())); - } let task = self.send_lsp_proto_request( buffer.clone(), upstream_client, project_id, - request, + GetCompletions { position, context }, cx, ); let language = buffer.read(cx).language().cloned(); @@ -5934,17 +5988,11 @@ impl LspStore { cx: &mut Context, ) -> Task> { let client = self.upstream_client(); + let buffer_id = buffer.read(cx).remote_id(); let buffer_snapshot = buffer.read(cx).snapshot(); - if !self.check_if_capable_for_proto_request( - &buffer, - GetCompletions::can_resolve_completions, - cx, - ) { - return Task::ready(Ok(false)); - } - cx.spawn(async move |lsp_store, cx| { + cx.spawn(async move |this, cx| { let mut did_resolve = false; if let Some((client, project_id)) = client { for completion_index in completion_indices { @@ -5981,7 +6029,7 @@ impl LspStore { completion.source.server_id() }; if let Some(server_id) = server_id { - let server_and_adapter = lsp_store + let server_and_adapter = this .read_with(cx, |lsp_store, _| { let server = lsp_store.language_server_for_id(server_id)?; let adapter = @@ -5996,6 +6044,7 @@ impl LspStore { let resolved = Self::resolve_completion_local( server, + &buffer_snapshot, completions.clone(), completion_index, ) @@ -6028,11 +6077,18 @@ impl LspStore { async fn resolve_completion_local( server: Arc, + snapshot: &BufferSnapshot, completions: Rc>>, completion_index: usize, ) -> Result<()> { let server_id = server.server_id(); - if !GetCompletions::can_resolve_completions(&server.capabilities()) { + let can_resolve = server + .capabilities() + .completion_provider + .as_ref() + .and_then(|options| options.resolve_provider) + .unwrap_or(false); + if !can_resolve { return Ok(()); } @@ -6066,8 +6122,26 @@ impl LspStore { .into_response() .context("resolve completion")?; - // We must not use any data such as sortText, filterText, insertText and textEdit to edit `Completion` since they are not suppose change during resolve. - // Refer: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion + if let Some(text_edit) = resolved_completion.text_edit.as_ref() { + // Technically we don't have to parse the whole `text_edit`, since the only + // language server we currently use that does update `text_edit` in `completionItem/resolve` + // is `typescript-language-server` and they only update `text_edit.new_text`. + // But we should not rely on that. + let edit = parse_completion_text_edit(text_edit, snapshot); + + if let Some(mut parsed_edit) = edit { + LineEnding::normalize(&mut parsed_edit.new_text); + + let mut completions = completions.borrow_mut(); + let completion = &mut completions[completion_index]; + + completion.new_text = parsed_edit.new_text; + completion.replace_range = parsed_edit.replace_range; + if let CompletionSource::Lsp { insert_range, .. } = &mut completion.source { + *insert_range = parsed_edit.insert_range; + } + } + } let mut completions = completions.borrow_mut(); let completion = &mut completions[completion_index]; @@ -6252,11 +6326,11 @@ impl LspStore { .old_replace_start .and_then(deserialize_anchor) .zip(response.old_replace_end.and_then(deserialize_anchor)); - if let Some((old_replace_start, old_replace_end)) = replace_range - && !response.new_text.is_empty() - { - completion.new_text = response.new_text; - completion.replace_range = old_replace_start..old_replace_end; + if let Some((old_replace_start, old_replace_end)) = replace_range { + if !response.new_text.is_empty() { + completion.new_text = response.new_text; + completion.replace_range = old_replace_start..old_replace_end; + } } Ok(()) @@ -6317,10 +6391,12 @@ impl LspStore { }) else { return Task::ready(Ok(None)); }; + let snapshot = buffer_handle.read(&cx).snapshot(); cx.spawn(async move |this, cx| { Self::resolve_completion_local( server.clone(), + &snapshot, completions.clone(), completion_index, ) @@ -6383,33 +6459,47 @@ impl LspStore { pub fn pull_diagnostics( &mut self, - buffer: Entity, + buffer_handle: Entity, cx: &mut Context, - ) -> Task>>> { - let buffer_id = buffer.read(cx).remote_id(); + ) -> Task>> { + let buffer = buffer_handle.read(cx); + let buffer_id = buffer.remote_id(); if let Some((client, upstream_project_id)) = self.upstream_client() { - let request = GetDocumentDiagnostics { - previous_result_id: None, - }; - if !self.is_capable_for_proto_request(&buffer, &request, cx) { - return Task::ready(Ok(None)); - } - let request_task = client.request_lsp( - upstream_project_id, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.to_proto(upstream_project_id, buffer.read(cx)), - ); + let request_task = client.request(proto::MultiLspQuery { + buffer_id: buffer_id.to_proto(), + version: serialize_version(&buffer_handle.read(cx).version()), + project_id: upstream_project_id, + strategy: Some(proto::multi_lsp_query::Strategy::All( + proto::AllLanguageServers {}, + )), + request: Some(proto::multi_lsp_query::Request::GetDocumentDiagnostics( + proto::GetDocumentDiagnostics { + project_id: upstream_project_id, + buffer_id: buffer_id.to_proto(), + version: serialize_version(&buffer_handle.read(cx).version()), + }, + )), + }); cx.background_spawn(async move { - // Proto requests cause the diagnostics to be pulled from language server(s) on the local side - // and then, buffer state updated with the diagnostics received, which will be later propagated to the client. - // Do not attempt to further process the dummy responses here. - let _response = request_task.await?; - Ok(None) + Ok(request_task + .await? + .responses + .into_iter() + .filter_map(|lsp_response| match lsp_response.response? { + proto::lsp_response::Response::GetDocumentDiagnosticsResponse(response) => { + Some(response) + } + unexpected => { + debug_panic!("Unexpected response: {unexpected:?}"); + None + } + }) + .flat_map(GetDocumentDiagnostics::diagnostics_from_proto) + .collect()) }) } else { - let server_ids = buffer.update(cx, |buffer, cx| { + let server_ids = buffer_handle.update(cx, |buffer, cx| { self.language_servers_for_local_buffer(buffer, cx) .map(|(_, server)| server.server_id()) .collect::>() @@ -6419,7 +6509,7 @@ impl LspStore { .map(|server_id| { let result_id = self.result_id(server_id, buffer_id, cx); self.request_lsp( - buffer.clone(), + buffer_handle.clone(), LanguageServerToQuery::Other(server_id), GetDocumentDiagnostics { previous_result_id: result_id, @@ -6434,43 +6524,41 @@ impl LspStore { for diagnostics in join_all(pull_diagnostics).await { responses.extend(diagnostics?); } - Ok(Some(responses)) + Ok(responses) }) } } pub fn inlay_hints( &mut self, - buffer: Entity, + buffer_handle: Entity, range: Range, cx: &mut Context, ) -> Task>> { + let buffer = buffer_handle.read(cx); let range_start = range.start; let range_end = range.end; - let buffer_id = buffer.read(cx).remote_id().into(); - let request = InlayHints { range }; + let buffer_id = buffer.remote_id().into(); + let lsp_request = InlayHints { range }; if let Some((client, project_id)) = self.upstream_client() { - if !self.is_capable_for_proto_request(&buffer, &request, cx) { - return Task::ready(Ok(Vec::new())); - } - let proto_request = proto::InlayHints { + let request = proto::InlayHints { project_id, buffer_id, start: Some(serialize_anchor(&range_start)), end: Some(serialize_anchor(&range_end)), - version: serialize_version(&buffer.read(cx).version()), + version: serialize_version(&buffer_handle.read(cx).version()), }; cx.spawn(async move |project, cx| { let response = client - .request(proto_request) + .request(request) .await .context("inlay hints proto request")?; LspCommand::response_from_proto( - request, + lsp_request, response, project.upgrade().context("No project")?, - buffer.clone(), + buffer_handle.clone(), cx.clone(), ) .await @@ -6478,13 +6566,13 @@ impl LspStore { }) } else { let lsp_request_task = self.request_lsp( - buffer.clone(), + buffer_handle.clone(), LanguageServerToQuery::FirstCapable, - request, + lsp_request, cx, ); cx.spawn(async move |_, cx| { - buffer + buffer_handle .update(cx, |buffer, _| { buffer.wait_for_edits(vec![range_start.timestamp, range_end.timestamp]) })? @@ -6500,93 +6588,75 @@ impl LspStore { buffer: Entity, cx: &mut Context, ) -> Task> { + let buffer_id = buffer.read(cx).remote_id(); let diagnostics = self.pull_diagnostics(buffer, cx); cx.spawn(async move |lsp_store, cx| { - let Some(diagnostics) = diagnostics.await.context("pulling diagnostics")? else { - return Ok(()); - }; + let diagnostics = diagnostics.await.context("pulling diagnostics")?; lsp_store.update(cx, |lsp_store, cx| { if lsp_store.as_local().is_none() { return; } - let mut unchanged_buffers = HashSet::default(); - let mut changed_buffers = HashSet::default(); - let server_diagnostics_updates = diagnostics - .into_iter() - .filter_map(|diagnostics_set| match diagnostics_set { - LspPullDiagnostics::Response { - server_id, - uri, - diagnostics, - } => Some((server_id, uri, diagnostics)), - LspPullDiagnostics::Default => None, - }) - .fold( - HashMap::default(), - |mut acc, (server_id, uri, diagnostics)| { - let (result_id, diagnostics) = match diagnostics { - PulledDiagnostics::Unchanged { result_id } => { - unchanged_buffers.insert(uri.clone()); - (Some(result_id), Vec::new()) - } - PulledDiagnostics::Changed { - result_id, - diagnostics, - } => { - changed_buffers.insert(uri.clone()); - (result_id, diagnostics) - } - }; - let disk_based_sources = Cow::Owned( - lsp_store - .language_server_adapter_for_id(server_id) - .as_ref() - .map(|adapter| adapter.disk_based_diagnostic_sources.as_slice()) - .unwrap_or(&[]) - .to_vec(), - ); - acc.entry(server_id).or_insert_with(Vec::new).push( - DocumentDiagnosticsUpdate { + for diagnostics_set in diagnostics { + let LspPullDiagnostics::Response { + server_id, + uri, + diagnostics, + } = diagnostics_set + else { + continue; + }; + + let adapter = lsp_store.language_server_adapter_for_id(server_id); + let disk_based_sources = adapter + .as_ref() + .map(|adapter| adapter.disk_based_diagnostic_sources.as_slice()) + .unwrap_or(&[]); + match diagnostics { + PulledDiagnostics::Unchanged { result_id } => { + lsp_store + .merge_diagnostics( server_id, - diagnostics: lsp::PublishDiagnosticsParams { - uri, + lsp::PublishDiagnosticsParams { + uri: uri.clone(), + diagnostics: Vec::new(), + version: None, + }, + Some(result_id), + DiagnosticSourceKind::Pulled, + disk_based_sources, + |_, _, _| true, + cx, + ) + .log_err(); + } + PulledDiagnostics::Changed { + diagnostics, + result_id, + } => { + lsp_store + .merge_diagnostics( + server_id, + lsp::PublishDiagnosticsParams { + uri: uri.clone(), diagnostics, version: None, }, result_id, + DiagnosticSourceKind::Pulled, disk_based_sources, - }, - ); - acc - }, - ); - - for diagnostic_updates in server_diagnostics_updates.into_values() { - lsp_store - .merge_lsp_diagnostics( - DiagnosticSourceKind::Pulled, - diagnostic_updates, - |buffer, old_diagnostic, cx| { - File::from_dyn(buffer.file()) - .and_then(|file| { - let abs_path = file.as_local()?.abs_path(cx); - lsp::Url::from_file_path(abs_path).ok() - }) - .is_none_or(|buffer_uri| { - unchanged_buffers.contains(&buffer_uri) - || match old_diagnostic.source_kind { - DiagnosticSourceKind::Pulled => { - !changed_buffers.contains(&buffer_uri) - } - DiagnosticSourceKind::Other - | DiagnosticSourceKind::Pushed => true, - } - }) - }, - cx, - ) - .log_err(); + |buffer, old_diagnostic, _| match old_diagnostic.source_kind { + DiagnosticSourceKind::Pulled => { + buffer.remote_id() != buffer_id + } + DiagnosticSourceKind::Other + | DiagnosticSourceKind::Pushed => true, + }, + cx, + ) + .log_err(); + } + } } }) }) @@ -6606,33 +6676,33 @@ impl LspStore { LspFetchStrategy::UseCache { known_cache_version, } => { - if let Some(cached_data) = self.lsp_document_colors.get(&buffer_id) - && !version_queried_for.changed_since(&cached_data.colors_for_version) - { - let has_different_servers = self.as_local().is_some_and(|local| { - local - .buffers_opened_in_servers - .get(&buffer_id) - .cloned() - .unwrap_or_default() - != cached_data.colors.keys().copied().collect() - }); - if !has_different_servers { - if Some(cached_data.cache_version) == known_cache_version { - return None; - } else { - return Some( - Task::ready(Ok(DocumentColors { - colors: cached_data - .colors - .values() - .flatten() - .cloned() - .collect(), - cache_version: Some(cached_data.cache_version), - })) - .shared(), - ); + if let Some(cached_data) = self.lsp_document_colors.get(&buffer_id) { + if !version_queried_for.changed_since(&cached_data.colors_for_version) { + let has_different_servers = self.as_local().is_some_and(|local| { + local + .buffers_opened_in_servers + .get(&buffer_id) + .cloned() + .unwrap_or_default() + != cached_data.colors.keys().copied().collect() + }); + if !has_different_servers { + if Some(cached_data.cache_version) == known_cache_version { + return None; + } else { + return Some( + Task::ready(Ok(DocumentColors { + colors: cached_data + .colors + .values() + .flatten() + .cloned() + .collect(), + cache_version: Some(cached_data.cache_version), + })) + .shared(), + ); + } } } } @@ -6640,10 +6710,10 @@ impl LspStore { } let lsp_data = self.lsp_document_colors.entry(buffer_id).or_default(); - if let Some((updating_for, running_update)) = &lsp_data.colors_update - && !version_queried_for.changed_since(updating_for) - { - return Some(running_update.clone()); + if let Some((updating_for, running_update)) = &lsp_data.colors_update { + if !version_queried_for.changed_since(&updating_for) { + return Some(running_update.clone()); + } } let query_version_queried_for = version_queried_for.clone(); let new_task = cx @@ -6690,18 +6760,16 @@ impl LspStore { .update(cx, |lsp_store, _| { let lsp_data = lsp_store.lsp_document_colors.entry(buffer_id).or_default(); - if let Some(fetched_colors) = fetched_colors { - if lsp_data.colors_for_version == query_version_queried_for { - lsp_data.colors.extend(fetched_colors); - lsp_data.cache_version += 1; - } else if !lsp_data - .colors_for_version - .changed_since(&query_version_queried_for) - { - lsp_data.colors_for_version = query_version_queried_for; - lsp_data.colors = fetched_colors; - lsp_data.cache_version += 1; - } + if lsp_data.colors_for_version == query_version_queried_for { + lsp_data.colors.extend(fetched_colors.clone()); + lsp_data.cache_version += 1; + } else if !lsp_data + .colors_for_version + .changed_since(&query_version_queried_for) + { + lsp_data.colors_for_version = query_version_queried_for; + lsp_data.colors = fetched_colors.clone(); + lsp_data.cache_version += 1; } lsp_data.colors_update = None; let colors = lsp_data @@ -6726,45 +6794,51 @@ impl LspStore { &mut self, buffer: &Entity, cx: &mut Context, - ) -> Task>>>> { + ) -> Task>>> { if let Some((client, project_id)) = self.upstream_client() { - let request = GetDocumentColor {}; - if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Ok(None)); - } - - let request_task = client.request_lsp( + let request_task = client.request(proto::MultiLspQuery { project_id, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.to_proto(project_id, buffer.read(cx)), - ); + buffer_id: buffer.read(cx).remote_id().to_proto(), + version: serialize_version(&buffer.read(cx).version()), + strategy: Some(proto::multi_lsp_query::Strategy::All( + proto::AllLanguageServers {}, + )), + request: Some(proto::multi_lsp_query::Request::GetDocumentColor( + GetDocumentColor {}.to_proto(project_id, buffer.read(cx)), + )), + }); let buffer = buffer.clone(); - cx.spawn(async move |lsp_store, cx| { - let Some(project) = lsp_store.upgrade() else { - return Ok(None); + cx.spawn(async move |project, cx| { + let Some(project) = project.upgrade() else { + return Ok(HashMap::default()); }; let colors = join_all( request_task .await .log_err() - .flatten() - .map(|response| response.payload) + .map(|response| response.responses) .unwrap_or_default() .into_iter() - .map(|color_response| { - let response = request.response_from_proto( - color_response.response, + .filter_map(|lsp_response| match lsp_response.response? { + proto::lsp_response::Response::GetDocumentColorResponse(response) => { + Some(( + LanguageServerId::from_proto(lsp_response.server_id), + response, + )) + } + unexpected => { + debug_panic!("Unexpected response: {unexpected:?}"); + None + } + }) + .map(|(server_id, color_response)| { + let response = GetDocumentColor {}.response_from_proto( + color_response, project.clone(), buffer.clone(), cx.clone(), ); - async move { - ( - LanguageServerId::from_proto(color_response.server_id), - response.await.log_err().unwrap_or_default(), - ) - } + async move { (server_id, response.await.log_err().unwrap_or_default()) } }), ) .await @@ -6775,25 +6849,23 @@ impl LspStore { .extend(colors); acc }); - Ok(Some(colors)) + Ok(colors) }) } else { let document_colors_task = self.request_multiple_lsp_locally(buffer, None::, GetDocumentColor, cx); cx.background_spawn(async move { - Ok(Some( - document_colors_task - .await - .into_iter() - .fold(HashMap::default(), |mut acc, (server_id, colors)| { - acc.entry(server_id) - .or_insert_with(HashSet::default) - .extend(colors); - acc - }) - .into_iter() - .collect(), - )) + Ok(document_colors_task + .await + .into_iter() + .fold(HashMap::default(), |mut acc, (server_id, colors)| { + acc.entry(server_id) + .or_insert_with(HashSet::default) + .extend(colors); + acc + }) + .into_iter() + .collect()) }) } } @@ -6803,34 +6875,45 @@ impl LspStore { buffer: &Entity, position: T, cx: &mut Context, - ) -> Task>> { + ) -> Task> { let position = position.to_point_utf16(buffer.read(cx)); if let Some((client, upstream_project_id)) = self.upstream_client() { - let request = GetSignatureHelp { position }; - if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(None); - } - let request_task = client.request_lsp( - upstream_project_id, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.to_proto(upstream_project_id, buffer.read(cx)), - ); + let request_task = client.request(proto::MultiLspQuery { + buffer_id: buffer.read(cx).remote_id().into(), + version: serialize_version(&buffer.read(cx).version()), + project_id: upstream_project_id, + strategy: Some(proto::multi_lsp_query::Strategy::All( + proto::AllLanguageServers {}, + )), + request: Some(proto::multi_lsp_query::Request::GetSignatureHelp( + GetSignatureHelp { position }.to_proto(upstream_project_id, buffer.read(cx)), + )), + }); let buffer = buffer.clone(); cx.spawn(async move |weak_project, cx| { - let project = weak_project.upgrade()?; - let signatures = join_all( + let Some(project) = weak_project.upgrade() else { + return Vec::new(); + }; + join_all( request_task .await .log_err() - .flatten() - .map(|response| response.payload) + .map(|response| response.responses) .unwrap_or_default() .into_iter() - .map(|response| { + .filter_map(|lsp_response| match lsp_response.response? { + proto::lsp_response::Response::GetSignatureHelpResponse(response) => { + Some(response) + } + unexpected => { + debug_panic!("Unexpected response: {unexpected:?}"); + None + } + }) + .map(|signature_response| { let response = GetSignatureHelp { position }.response_from_proto( - response.response, + signature_response, project.clone(), buffer.clone(), cx.clone(), @@ -6841,8 +6924,7 @@ impl LspStore { .await .into_iter() .flatten() - .collect(); - Some(signatures) + .collect() }) } else { let all_actions_task = self.request_multiple_lsp_locally( @@ -6852,13 +6934,11 @@ impl LspStore { cx, ); cx.background_spawn(async move { - Some( - all_actions_task - .await - .into_iter() - .flat_map(|(_, actions)| actions) - .collect::>(), - ) + all_actions_task + .await + .into_iter() + .flat_map(|(_, actions)| actions) + .collect::>() }) } } @@ -6868,32 +6948,43 @@ impl LspStore { buffer: &Entity, position: PointUtf16, cx: &mut Context, - ) -> Task>> { + ) -> Task> { if let Some((client, upstream_project_id)) = self.upstream_client() { - let request = GetHover { position }; - if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(None); - } - let request_task = client.request_lsp( - upstream_project_id, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.to_proto(upstream_project_id, buffer.read(cx)), - ); + let request_task = client.request(proto::MultiLspQuery { + buffer_id: buffer.read(cx).remote_id().into(), + version: serialize_version(&buffer.read(cx).version()), + project_id: upstream_project_id, + strategy: Some(proto::multi_lsp_query::Strategy::All( + proto::AllLanguageServers {}, + )), + request: Some(proto::multi_lsp_query::Request::GetHover( + GetHover { position }.to_proto(upstream_project_id, buffer.read(cx)), + )), + }); let buffer = buffer.clone(); cx.spawn(async move |weak_project, cx| { - let project = weak_project.upgrade()?; - let hovers = join_all( + let Some(project) = weak_project.upgrade() else { + return Vec::new(); + }; + join_all( request_task .await .log_err() - .flatten() - .map(|response| response.payload) + .map(|response| response.responses) .unwrap_or_default() .into_iter() - .map(|response| { + .filter_map(|lsp_response| match lsp_response.response? { + proto::lsp_response::Response::GetHoverResponse(response) => { + Some(response) + } + unexpected => { + debug_panic!("Unexpected response: {unexpected:?}"); + None + } + }) + .map(|hover_response| { let response = GetHover { position }.response_from_proto( - response.response, + hover_response, project.clone(), buffer.clone(), cx.clone(), @@ -6910,8 +7001,7 @@ impl LspStore { .await .into_iter() .flatten() - .collect(); - Some(hovers) + .collect() }) } else { let all_actions_task = self.request_multiple_lsp_locally( @@ -6921,13 +7011,11 @@ impl LspStore { cx, ); cx.background_spawn(async move { - Some( - all_actions_task - .await - .into_iter() - .filter_map(|(_, hover)| remove_empty_hover_blocks(hover?)) - .collect::>(), - ) + all_actions_task + .await + .into_iter() + .filter_map(|(_, hover)| remove_empty_hover_blocks(hover?)) + .collect::>() }) } } @@ -6963,11 +7051,11 @@ impl LspStore { let mut requests = Vec::new(); let mut requested_servers = BTreeSet::new(); - for (seed, state) in local.language_server_ids.iter() { + 'next_server: for ((worktree_id, _), server_ids) in local.language_server_ids.iter() { let Some(worktree_handle) = self .worktree_store .read(cx) - .worktree_for_id(seed.worktree_id, cx) + .worktree_for_id(*worktree_id, cx) else { continue; }; @@ -6976,30 +7064,31 @@ impl LspStore { continue; } - if !requested_servers.insert(state.id) { - continue; - } + let mut servers_to_query = server_ids + .difference(&requested_servers) + .cloned() + .collect::>(); + for server_id in &servers_to_query { + let (lsp_adapter, server) = match local.language_servers.get(server_id) { + Some(LanguageServerState::Running { + adapter, server, .. + }) => (adapter.clone(), server), - let (lsp_adapter, server) = match local.language_servers.get(&state.id) { - Some(LanguageServerState::Running { - adapter, server, .. - }) => (adapter.clone(), server), - - _ => continue, - }; - let supports_workspace_symbol_request = - match server.capabilities().workspace_symbol_provider { - Some(OneOf::Left(supported)) => supported, - Some(OneOf::Right(_)) => true, - None => false, + _ => continue 'next_server, }; - if !supports_workspace_symbol_request { - continue; - } - let worktree_abs_path = worktree.abs_path().clone(); - let worktree_handle = worktree_handle.clone(); - let server_id = server.server_id(); - requests.push( + let supports_workspace_symbol_request = + match server.capabilities().workspace_symbol_provider { + Some(OneOf::Left(supported)) => supported, + Some(OneOf::Right(_)) => true, + None => false, + }; + if !supports_workspace_symbol_request { + continue 'next_server; + } + let worktree_abs_path = worktree.abs_path().clone(); + let worktree_handle = worktree_handle.clone(); + let server_id = server.server_id(); + requests.push( server .request::( lsp::WorkspaceSymbolParams { @@ -7041,6 +7130,8 @@ impl LspStore { } }), ); + } + requested_servers.append(&mut servers_to_query); } cx.spawn(async move |this, cx| { @@ -7069,7 +7160,7 @@ impl LspStore { worktree = tree; path = rel_path; } else { - worktree = source_worktree; + worktree = source_worktree.clone(); path = relativize_path(&result.worktree_abs_path, &abs_path); } @@ -7138,7 +7229,7 @@ impl LspStore { include_ignored || worktree .entry_for_path(path.as_ref()) - .is_some_and(|entry| !entry.is_ignored) + .map_or(false, |entry| !entry.is_ignored) }) .flat_map(move |(path, summaries)| { summaries.iter().map(move |(server_id, summary)| { @@ -7187,9 +7278,7 @@ impl LspStore { let build_incremental_change = || { buffer - .edits_since::>( - previous_snapshot.snapshot.version(), - ) + .edits_since::<(PointUtf16, usize)>(previous_snapshot.snapshot.version()) .map(|edit| { let edit_start = edit.new.start.0; let edit_end = edit_start + (edit.old.end.0 - edit.old.start.0); @@ -7303,7 +7392,7 @@ impl LspStore { None } - async fn refresh_workspace_configurations( + pub(crate) async fn refresh_workspace_configurations( lsp_store: &WeakEntity, fs: Arc, cx: &mut AsyncApp, @@ -7312,83 +7401,90 @@ impl LspStore { let mut refreshed_servers = HashSet::default(); let servers = lsp_store .update(cx, |lsp_store, cx| { - let local = lsp_store.as_local()?; - - let servers = local + let toolchain_store = lsp_store.toolchain_store(cx); + let Some(local) = lsp_store.as_local() else { + return Vec::default(); + }; + local .language_server_ids .iter() - .filter_map(|(seed, state)| { + .flat_map(|((worktree_id, _), server_ids)| { let worktree = lsp_store .worktree_store .read(cx) - .worktree_for_id(seed.worktree_id, cx); - let delegate: Arc = - worktree.map(|worktree| { - LocalLspAdapterDelegate::new( - local.languages.clone(), - &local.environment, - cx.weak_entity(), - &worktree, - local.http_client.clone(), - local.fs.clone(), - cx, - ) - })?; - let server_id = state.id; + .worktree_for_id(*worktree_id, cx); + let delegate = worktree.map(|worktree| { + LocalLspAdapterDelegate::new( + local.languages.clone(), + &local.environment, + cx.weak_entity(), + &worktree, + local.http_client.clone(), + local.fs.clone(), + cx, + ) + }); - let states = local.language_servers.get(&server_id)?; + let fs = fs.clone(); + let toolchain_store = toolchain_store.clone(); + server_ids.iter().filter_map(|server_id| { + let delegate = delegate.clone()? as Arc; + let states = local.language_servers.get(server_id)?; - match states { - LanguageServerState::Starting { .. } => None, - LanguageServerState::Running { - adapter, server, .. - } => { - let fs = fs.clone(); - - let adapter = adapter.clone(); - let server = server.clone(); - refreshed_servers.insert(server.name()); - let toolchain = seed.toolchain.clone(); - Some(cx.spawn(async move |_, cx| { - let settings = - LocalLspStore::workspace_configuration_for_adapter( - adapter.adapter.clone(), - fs.as_ref(), - &delegate, - toolchain, - cx, - ) - .await - .ok()?; - server - .notify::( - &lsp::DidChangeConfigurationParams { settings }, - ) - .ok()?; - Some(()) - })) + match states { + LanguageServerState::Starting { .. } => None, + LanguageServerState::Running { + adapter, server, .. + } => { + let fs = fs.clone(); + let toolchain_store = toolchain_store.clone(); + let adapter = adapter.clone(); + let server = server.clone(); + refreshed_servers.insert(server.name()); + Some(cx.spawn(async move |_, cx| { + let settings = + LocalLspStore::workspace_configuration_for_adapter( + adapter.adapter.clone(), + fs.as_ref(), + &delegate, + toolchain_store, + cx, + ) + .await + .ok()?; + server + .notify::( + &lsp::DidChangeConfigurationParams { settings }, + ) + .ok()?; + Some(()) + })) + } } - } + }).collect::>() }) - .collect::>(); - - Some(servers) + .collect::>() }) - .ok() - .flatten()?; + .ok()?; - log::debug!("Refreshing workspace configurations for servers {refreshed_servers:?}"); + log::info!("Refreshing workspace configurations for servers {refreshed_servers:?}"); // TODO this asynchronous job runs concurrently with extension (de)registration and may take enough time for a certain extension // to stop and unregister its language server wrapper. // This is racy : an extension might have already removed all `local.language_servers` state, but here we `.clone()` and hold onto it anyway. // This now causes errors in the logs, we should find a way to remove such servers from the processing everywhere. let _: Vec> = join_all(servers).await; - Some(()) }) .await; } + fn toolchain_store(&self, cx: &App) -> Arc { + if let Some(toolchain_store) = self.toolchain_store.as_ref() { + toolchain_store.read(cx).as_language_toolchain_store() + } else { + Arc::new(EmptyToolchainStore) + } + } fn maintain_workspace_config( fs: Arc, external_refresh_requests: watch::Receiver<()>, @@ -7403,19 +7499,8 @@ impl LspStore { let mut joint_future = futures::stream::select(settings_changed_rx, external_refresh_requests); - // Multiple things can happen when a workspace environment (selected toolchain + settings) change: - // - We might shut down a language server if it's no longer enabled for a given language (and there are no buffers using it otherwise). - // - We might also shut it down when the workspace configuration of all of the users of a given language server converges onto that of the other. - // - In the same vein, we might also decide to start a new language server if the workspace configuration *diverges* from the other. - // - In the easiest case (where we're not wrangling the lifetime of a language server anyhow), if none of the roots of a single language server diverge in their configuration, - // but it is still different to what we had before, we're gonna send out a workspace configuration update. cx.spawn(async move |this, cx| { while let Some(()) = joint_future.next().await { - this.update(cx, |this, cx| { - this.refresh_server_tree(cx); - }) - .ok(); - Self::refresh_workspace_configurations(&this, fs.clone(), cx).await; } @@ -7476,20 +7561,16 @@ impl LspStore { self.downstream_client = Some((downstream_client.clone(), project_id)); for (server_id, status) in &self.language_server_statuses { - if let Some(server) = self.language_server_for_id(*server_id) { - downstream_client - .send(proto::StartLanguageServer { - project_id, - server: Some(proto::LanguageServer { - id: server_id.to_proto(), - name: status.name.to_string(), - worktree_id: None, - }), - capabilities: serde_json::to_string(&server.capabilities()) - .expect("serializing server LSP capabilities"), - }) - .log_err(); - } + downstream_client + .send(proto::StartLanguageServer { + project_id, + server: Some(proto::LanguageServer { + id: server_id.0 as u64, + name: status.name.clone(), + worktree_id: None, + }), + }) + .log_err(); } } @@ -7509,21 +7590,14 @@ impl LspStore { pub(crate) fn set_language_server_statuses_from_proto( &mut self, language_servers: Vec, - server_capabilities: Vec, ) { self.language_server_statuses = language_servers .into_iter() - .zip(server_capabilities) - .map(|(server, server_capabilities)| { - let server_id = LanguageServerId(server.id as usize); - if let Ok(server_capabilities) = serde_json::from_str(&server_capabilities) { - self.lsp_server_capabilities - .insert(server_id, server_capabilities); - } + .map(|server| { ( - server_id, + LanguageServerId(server.id as usize), LanguageServerStatus { - name: LanguageServerName::from_proto(server.name), + name: server.name, pending_work: Default::default(), has_pending_diagnostic_updates: false, progress_tokens: Default::default(), @@ -7533,6 +7607,47 @@ impl LspStore { .collect(); } + fn register_local_language_server( + &mut self, + worktree: Entity, + language_server_name: LanguageServerName, + language_server_id: LanguageServerId, + cx: &mut App, + ) { + let Some(local) = self.as_local_mut() else { + return; + }; + + let worktree_id = worktree.read(cx).id(); + if worktree.read(cx).is_visible() { + let path = ProjectPath { + worktree_id, + path: Arc::from("".as_ref()), + }; + let delegate = Arc::new(ManifestQueryDelegate::new(worktree.read(cx).snapshot())); + local.lsp_tree.update(cx, |language_server_tree, cx| { + for node in language_server_tree.get( + path, + AdapterQuery::Adapter(&language_server_name), + delegate, + cx, + ) { + node.server_id_or_init(|disposition| { + assert_eq!(disposition.server_name, &language_server_name); + + language_server_id + }); + } + }); + } + + local + .language_server_ids + .entry((worktree_id, language_server_name)) + .or_default() + .insert(language_server_id); + } + #[cfg(test)] pub fn update_diagnostic_entries( &mut self, @@ -7544,132 +7659,88 @@ impl LspStore { cx: &mut Context, ) -> anyhow::Result<()> { self.merge_diagnostic_entries( - vec![DocumentDiagnosticsUpdate { - diagnostics: DocumentDiagnostics { - diagnostics, - document_abs_path: abs_path, - version, - }, - result_id, - server_id, - disk_based_sources: Cow::Borrowed(&[]), - }], + server_id, + abs_path, + result_id, + version, + diagnostics, |_, _, _| false, cx, )?; Ok(()) } - pub fn merge_diagnostic_entries<'a>( + pub fn merge_diagnostic_entries( &mut self, - diagnostic_updates: Vec>, - merge: impl Fn(&Buffer, &Diagnostic, &App) -> bool + Clone, + server_id: LanguageServerId, + abs_path: PathBuf, + result_id: Option, + version: Option, + mut diagnostics: Vec>>, + filter: impl Fn(&Buffer, &Diagnostic, &App) -> bool + Clone, cx: &mut Context, ) -> anyhow::Result<()> { - let mut diagnostics_summary = None::; - let mut updated_diagnostics_paths = HashMap::default(); - for mut update in diagnostic_updates { - let abs_path = &update.diagnostics.document_abs_path; - let server_id = update.server_id; - let Some((worktree, relative_path)) = - self.worktree_store.read(cx).find_worktree(abs_path, cx) - else { - log::warn!("skipping diagnostics update, no worktree found for path {abs_path:?}"); - return Ok(()); - }; + let Some((worktree, relative_path)) = + self.worktree_store.read(cx).find_worktree(&abs_path, cx) + else { + log::warn!("skipping diagnostics update, no worktree found for path {abs_path:?}"); + return Ok(()); + }; - let worktree_id = worktree.read(cx).id(); - let project_path = ProjectPath { - worktree_id, - path: relative_path.into(), - }; + let project_path = ProjectPath { + worktree_id: worktree.read(cx).id(), + path: relative_path.into(), + }; - if let Some(buffer_handle) = self.buffer_store.read(cx).get_by_path(&project_path) { - let snapshot = buffer_handle.read(cx).snapshot(); - let buffer = buffer_handle.read(cx); - let reused_diagnostics = buffer - .buffer_diagnostics(Some(server_id)) - .iter() - .filter(|v| merge(buffer, &v.diagnostic, cx)) - .map(|v| { - let start = Unclipped(v.range.start.to_point_utf16(&snapshot)); - let end = Unclipped(v.range.end.to_point_utf16(&snapshot)); - DiagnosticEntry { - range: start..end, - diagnostic: v.diagnostic.clone(), - } - }) - .collect::>(); + if let Some(buffer_handle) = self.buffer_store.read(cx).get_by_path(&project_path) { + let snapshot = buffer_handle.read(cx).snapshot(); + let buffer = buffer_handle.read(cx); + let reused_diagnostics = buffer + .get_diagnostics(server_id) + .into_iter() + .flat_map(|diag| { + diag.iter() + .filter(|v| filter(buffer, &v.diagnostic, cx)) + .map(|v| { + let start = Unclipped(v.range.start.to_point_utf16(&snapshot)); + let end = Unclipped(v.range.end.to_point_utf16(&snapshot)); + DiagnosticEntry { + range: start..end, + diagnostic: v.diagnostic.clone(), + } + }) + }) + .collect::>(); - self.as_local_mut() - .context("cannot merge diagnostics on a remote LspStore")? - .update_buffer_diagnostics( - &buffer_handle, - server_id, - update.result_id, - update.diagnostics.version, - update.diagnostics.diagnostics.clone(), - reused_diagnostics.clone(), - cx, - )?; - - update.diagnostics.diagnostics.extend(reused_diagnostics); - } - - let updated = worktree.update(cx, |worktree, cx| { - self.update_worktree_diagnostics( - worktree.id(), + self.as_local_mut() + .context("cannot merge diagnostics on a remote LspStore")? + .update_buffer_diagnostics( + &buffer_handle, server_id, - project_path.path.clone(), - update.diagnostics.diagnostics, + result_id, + version, + diagnostics.clone(), + reused_diagnostics.clone(), cx, - ) - })?; - match updated { - ControlFlow::Continue(new_summary) => { - if let Some((project_id, new_summary)) = new_summary { - match &mut diagnostics_summary { - Some(diagnostics_summary) => { - diagnostics_summary - .more_summaries - .push(proto::DiagnosticSummary { - path: project_path.path.as_ref().to_proto(), - language_server_id: server_id.0 as u64, - error_count: new_summary.error_count, - warning_count: new_summary.warning_count, - }) - } - None => { - diagnostics_summary = Some(proto::UpdateDiagnosticSummary { - project_id, - worktree_id: worktree_id.to_proto(), - summary: Some(proto::DiagnosticSummary { - path: project_path.path.as_ref().to_proto(), - language_server_id: server_id.0 as u64, - error_count: new_summary.error_count, - warning_count: new_summary.warning_count, - }), - more_summaries: Vec::new(), - }) - } - } - } - updated_diagnostics_paths - .entry(server_id) - .or_insert_with(Vec::new) - .push(project_path); - } - ControlFlow::Break(()) => {} - } + )?; + + diagnostics.extend(reused_diagnostics); } - if let Some((diagnostics_summary, (downstream_client, _))) = - diagnostics_summary.zip(self.downstream_client.as_ref()) - { - downstream_client.send(diagnostics_summary).log_err(); - } - for (server_id, paths) in updated_diagnostics_paths { - cx.emit(LspStoreEvent::DiagnosticsUpdated { server_id, paths }); + let updated = worktree.update(cx, |worktree, cx| { + self.update_worktree_diagnostics( + worktree.id(), + server_id, + project_path.path.clone(), + diagnostics, + cx, + ) + })?; + if updated { + cx.emit(LspStoreEvent::DiagnosticsUpdated { + language_server_id: server_id, + path: project_path, + }) } Ok(()) } @@ -7678,10 +7749,10 @@ impl LspStore { &mut self, worktree_id: WorktreeId, server_id: LanguageServerId, - path_in_worktree: Arc, + worktree_path: Arc, diagnostics: Vec>>, _: &mut Context, - ) -> Result>> { + ) -> Result { let local = match &mut self.mode { LspStoreMode::Local(local_lsp_store) => local_lsp_store, _ => anyhow::bail!("update_worktree_diagnostics called on remote"), @@ -7689,9 +7760,7 @@ impl LspStore { let summaries_for_tree = self.diagnostic_summaries.entry(worktree_id).or_default(); let diagnostics_for_tree = local.diagnostics.entry(worktree_id).or_default(); - let summaries_by_server_id = summaries_for_tree - .entry(path_in_worktree.clone()) - .or_default(); + let summaries_by_server_id = summaries_for_tree.entry(worktree_path.clone()).or_default(); let old_summary = summaries_by_server_id .remove(&server_id) @@ -7699,19 +7768,18 @@ impl LspStore { let new_summary = DiagnosticSummary::new(&diagnostics); if new_summary.is_empty() { - if let Some(diagnostics_by_server_id) = diagnostics_for_tree.get_mut(&path_in_worktree) - { + if let Some(diagnostics_by_server_id) = diagnostics_for_tree.get_mut(&worktree_path) { if let Ok(ix) = diagnostics_by_server_id.binary_search_by_key(&server_id, |e| e.0) { diagnostics_by_server_id.remove(ix); } if diagnostics_by_server_id.is_empty() { - diagnostics_for_tree.remove(&path_in_worktree); + diagnostics_for_tree.remove(&worktree_path); } } } else { summaries_by_server_id.insert(server_id, new_summary); let diagnostics_by_server_id = diagnostics_for_tree - .entry(path_in_worktree.clone()) + .entry(worktree_path.clone()) .or_default(); match diagnostics_by_server_id.binary_search_by_key(&server_id, |e| e.0) { Ok(ix) => { @@ -7724,22 +7792,23 @@ impl LspStore { } if !old_summary.is_empty() || !new_summary.is_empty() { - if let Some((_, project_id)) = &self.downstream_client { - Ok(ControlFlow::Continue(Some(( - *project_id, - proto::DiagnosticSummary { - path: path_in_worktree.to_proto(), - language_server_id: server_id.0 as u64, - error_count: new_summary.error_count as u32, - warning_count: new_summary.warning_count as u32, - }, - )))) - } else { - Ok(ControlFlow::Continue(None)) + if let Some((downstream_client, project_id)) = &self.downstream_client { + downstream_client + .send(proto::UpdateDiagnosticSummary { + project_id: *project_id, + worktree_id: worktree_id.to_proto(), + summary: Some(proto::DiagnosticSummary { + path: worktree_path.to_proto(), + language_server_id: server_id.0 as u64, + error_count: new_summary.error_count as u32, + warning_count: new_summary.warning_count as u32, + }), + }) + .log_err(); } - } else { - Ok(ControlFlow::Break(())) } + + Ok(!old_summary.is_empty() || !new_summary.is_empty()) } pub fn open_buffer_for_symbol( @@ -7759,12 +7828,17 @@ impl LspStore { .await }) } else if let Some(local) = self.as_local() { - let is_valid = local.language_server_ids.iter().any(|(seed, state)| { - seed.worktree_id == symbol.source_worktree_id - && state.id == symbol.source_language_server_id - && symbol.language_server_name == seed.name - }); - if !is_valid { + let Some(language_server_id) = local + .language_server_ids + .get(&( + symbol.source_worktree_id, + symbol.language_server_name.clone(), + )) + .and_then(|ids| { + ids.contains(&symbol.source_language_server_id) + .then_some(symbol.source_language_server_id) + }) + else { return Task::ready(Err(anyhow!( "language server for worktree and language not found" ))); @@ -7788,16 +7862,22 @@ impl LspStore { return Task::ready(Err(anyhow!("invalid symbol path"))); }; - self.open_local_buffer_via_lsp(symbol_uri, symbol.source_language_server_id, cx) + self.open_local_buffer_via_lsp( + symbol_uri, + language_server_id, + symbol.language_server_name.clone(), + cx, + ) } else { Task::ready(Err(anyhow!("no upstream client or local store"))) } } - pub(crate) fn open_local_buffer_via_lsp( + pub fn open_local_buffer_via_lsp( &mut self, mut abs_path: lsp::Url, language_server_id: LanguageServerId, + language_server_name: LanguageServerName, cx: &mut Context, ) -> Task>> { cx.spawn(async move |lsp_store, cx| { @@ -7848,13 +7928,12 @@ impl LspStore { if worktree.read_with(cx, |worktree, _| worktree.is_local())? { lsp_store .update(cx, |lsp_store, cx| { - if let Some(local) = lsp_store.as_local_mut() { - local.register_language_server_for_invisible_worktree( - &worktree, - language_server_id, - cx, - ) - } + lsp_store.register_local_language_server( + worktree.clone(), + language_server_name, + language_server_id, + cx, + ) }) .ok(); } @@ -7987,209 +8066,12 @@ impl LspStore { })? } - async fn handle_lsp_query( - lsp_store: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - use proto::lsp_query::Request; - let sender_id = envelope.original_sender_id().unwrap_or_default(); - let lsp_query = envelope.payload; - let lsp_request_id = LspRequestId(lsp_query.lsp_request_id); - match lsp_query.request.context("invalid LSP query request")? { - Request::GetReferences(get_references) => { - let position = get_references.position.clone().and_then(deserialize_anchor); - Self::query_lsp_locally::( - lsp_store, - sender_id, - lsp_request_id, - get_references, - position, - cx.clone(), - ) - .await?; - } - Request::GetDocumentColor(get_document_color) => { - Self::query_lsp_locally::( - lsp_store, - sender_id, - lsp_request_id, - get_document_color, - None, - cx.clone(), - ) - .await?; - } - Request::GetHover(get_hover) => { - let position = get_hover.position.clone().and_then(deserialize_anchor); - Self::query_lsp_locally::( - lsp_store, - sender_id, - lsp_request_id, - get_hover, - position, - cx.clone(), - ) - .await?; - } - Request::GetCodeActions(get_code_actions) => { - Self::query_lsp_locally::( - lsp_store, - sender_id, - lsp_request_id, - get_code_actions, - None, - cx.clone(), - ) - .await?; - } - Request::GetSignatureHelp(get_signature_help) => { - let position = get_signature_help - .position - .clone() - .and_then(deserialize_anchor); - Self::query_lsp_locally::( - lsp_store, - sender_id, - lsp_request_id, - get_signature_help, - position, - cx.clone(), - ) - .await?; - } - Request::GetCodeLens(get_code_lens) => { - Self::query_lsp_locally::( - lsp_store, - sender_id, - lsp_request_id, - get_code_lens, - None, - cx.clone(), - ) - .await?; - } - Request::GetDefinition(get_definition) => { - let position = get_definition.position.clone().and_then(deserialize_anchor); - Self::query_lsp_locally::( - lsp_store, - sender_id, - lsp_request_id, - get_definition, - position, - cx.clone(), - ) - .await?; - } - Request::GetDeclaration(get_declaration) => { - let position = get_declaration - .position - .clone() - .and_then(deserialize_anchor); - Self::query_lsp_locally::( - lsp_store, - sender_id, - lsp_request_id, - get_declaration, - position, - cx.clone(), - ) - .await?; - } - Request::GetTypeDefinition(get_type_definition) => { - let position = get_type_definition - .position - .clone() - .and_then(deserialize_anchor); - Self::query_lsp_locally::( - lsp_store, - sender_id, - lsp_request_id, - get_type_definition, - position, - cx.clone(), - ) - .await?; - } - Request::GetImplementation(get_implementation) => { - let position = get_implementation - .position - .clone() - .and_then(deserialize_anchor); - Self::query_lsp_locally::( - lsp_store, - sender_id, - lsp_request_id, - get_implementation, - position, - cx.clone(), - ) - .await?; - } - // Diagnostics pull synchronizes internally via the buffer state, and cannot be handled generically as the other requests. - Request::GetDocumentDiagnostics(get_document_diagnostics) => { - let buffer_id = BufferId::new(get_document_diagnostics.buffer_id())?; - let version = deserialize_version(get_document_diagnostics.buffer_version()); - let buffer = lsp_store.update(&mut cx, |this, cx| { - this.buffer_store.read(cx).get_existing(buffer_id) - })??; - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(version.clone()) - })? - .await?; - lsp_store.update(&mut cx, |lsp_store, cx| { - let existing_queries = lsp_store - .running_lsp_requests - .entry(TypeId::of::()) - .or_default(); - if ::ProtoRequest::stop_previous_requests( - ) || buffer.read(cx).version.changed_since(&existing_queries.0) - { - existing_queries.1.clear(); - } - existing_queries.1.insert( - lsp_request_id, - cx.spawn(async move |lsp_store, cx| { - let diagnostics_pull = lsp_store - .update(cx, |lsp_store, cx| { - lsp_store.pull_diagnostics_for_buffer(buffer, cx) - }) - .ok(); - if let Some(diagnostics_pull) = diagnostics_pull { - match diagnostics_pull.await { - Ok(()) => {} - Err(e) => log::error!("Failed to pull diagnostics: {e:#}"), - }; - } - }), - ); - })?; - } - } - Ok(proto::Ack {}) - } - - async fn handle_lsp_query_response( - lsp_store: Entity, - envelope: TypedEnvelope, - cx: AsyncApp, - ) -> Result<()> { - lsp_store.read_with(&cx, |lsp_store, _| { - if let Some((upstream_client, _)) = lsp_store.upstream_client() { - upstream_client.handle_lsp_response(envelope.clone()); - } - })?; - Ok(()) - } - - // todo(lsp) remove after Zed Stable hits v0.204.x async fn handle_multi_lsp_query( lsp_store: Entity, envelope: TypedEnvelope, mut cx: AsyncApp, ) -> Result { - let response_from_ssh = lsp_store.read_with(&cx, |this, _| { + let response_from_ssh = lsp_store.read_with(&mut cx, |this, _| { let (upstream_client, project_id) = this.upstream_client()?; let mut payload = envelope.payload.clone(); payload.project_id = project_id; @@ -8211,7 +8093,7 @@ impl LspStore { buffer.wait_for_version(version.clone()) })? .await?; - let buffer_version = buffer.read_with(&cx, |buffer, _| buffer.version())?; + let buffer_version = buffer.read_with(&mut cx, |buffer, _| buffer.version())?; match envelope .payload .strategy @@ -8730,6 +8612,34 @@ impl LspStore { Ok(proto::Ack {}) } + async fn handle_language_server_id_for_name( + lsp_store: Entity, + envelope: TypedEnvelope, + mut cx: AsyncApp, + ) -> Result { + let name = &envelope.payload.name; + let buffer_id = BufferId::new(envelope.payload.buffer_id)?; + lsp_store + .update(&mut cx, |lsp_store, cx| { + let buffer = lsp_store.buffer_store.read(cx).get_existing(buffer_id)?; + let server_id = buffer.update(cx, |buffer, cx| { + lsp_store + .language_servers_for_local_buffer(buffer, cx) + .find_map(|(adapter, server)| { + if adapter.name.0.as_ref() == name { + Some(server.server_id()) + } else { + None + } + }) + }); + Ok(server_id) + })? + .map(|server_id| proto::LanguageServerIdForNameResponse { + server_id: server_id.map(|id| id.to_proto()), + }) + } + async fn handle_rename_project_entry( this: Entity, envelope: TypedEnvelope, @@ -8752,12 +8662,12 @@ impl LspStore { })? .context("worktree not found")?; let (old_abs_path, new_abs_path) = { - let root_path = worktree.read_with(&cx, |this, _| this.abs_path())?; + let root_path = worktree.read_with(&mut cx, |this, _| this.abs_path())?; let new_path = PathBuf::from_proto(envelope.payload.new_path.clone()); (root_path.join(&old_path), root_path.join(&new_path)) }; - let _transaction = Self::will_rename_entry( + Self::will_rename_entry( this.downgrade(), worktree_id, &old_abs_path, @@ -8767,7 +8677,7 @@ impl LspStore { ) .await; let response = Worktree::handle_rename_entry(worktree, envelope.payload, cx.clone()).await; - this.read_with(&cx, |this, _| { + this.read_with(&mut cx, |this, _| { this.did_rename_entry(worktree_id, &old_abs_path, &new_abs_path, is_dir); }) .ok(); @@ -8779,116 +8689,75 @@ impl LspStore { envelope: TypedEnvelope, mut cx: AsyncApp, ) -> Result<()> { - this.update(&mut cx, |lsp_store, cx| { + this.update(&mut cx, |this, cx| { let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); - let mut updated_diagnostics_paths = HashMap::default(); - let mut diagnostics_summary = None::; - for message_summary in envelope - .payload - .summary - .into_iter() - .chain(envelope.payload.more_summaries) - { + if let Some(message) = envelope.payload.summary { let project_path = ProjectPath { worktree_id, - path: Arc::::from_proto(message_summary.path), + path: Arc::::from_proto(message.path), }; let path = project_path.path.clone(); - let server_id = LanguageServerId(message_summary.language_server_id as usize); + let server_id = LanguageServerId(message.language_server_id as usize); let summary = DiagnosticSummary { - error_count: message_summary.error_count as usize, - warning_count: message_summary.warning_count as usize, + error_count: message.error_count as usize, + warning_count: message.warning_count as usize, }; if summary.is_empty() { if let Some(worktree_summaries) = - lsp_store.diagnostic_summaries.get_mut(&worktree_id) - && let Some(summaries) = worktree_summaries.get_mut(&path) + this.diagnostic_summaries.get_mut(&worktree_id) { - summaries.remove(&server_id); - if summaries.is_empty() { - worktree_summaries.remove(&path); + if let Some(summaries) = worktree_summaries.get_mut(&path) { + summaries.remove(&server_id); + if summaries.is_empty() { + worktree_summaries.remove(&path); + } } } } else { - lsp_store - .diagnostic_summaries + this.diagnostic_summaries .entry(worktree_id) .or_default() .entry(path) .or_default() .insert(server_id, summary); } - - if let Some((_, project_id)) = &lsp_store.downstream_client { - match &mut diagnostics_summary { - Some(diagnostics_summary) => { - diagnostics_summary - .more_summaries - .push(proto::DiagnosticSummary { - path: project_path.path.as_ref().to_proto(), - language_server_id: server_id.0 as u64, - error_count: summary.error_count as u32, - warning_count: summary.warning_count as u32, - }) - } - None => { - diagnostics_summary = Some(proto::UpdateDiagnosticSummary { - project_id: *project_id, - worktree_id: worktree_id.to_proto(), - summary: Some(proto::DiagnosticSummary { - path: project_path.path.as_ref().to_proto(), - language_server_id: server_id.0 as u64, - error_count: summary.error_count as u32, - warning_count: summary.warning_count as u32, - }), - more_summaries: Vec::new(), - }) - } - } + if let Some((downstream_client, project_id)) = &this.downstream_client { + downstream_client + .send(proto::UpdateDiagnosticSummary { + project_id: *project_id, + worktree_id: worktree_id.to_proto(), + summary: Some(proto::DiagnosticSummary { + path: project_path.path.as_ref().to_proto(), + language_server_id: server_id.0 as u64, + error_count: summary.error_count as u32, + warning_count: summary.warning_count as u32, + }), + }) + .log_err(); } - updated_diagnostics_paths - .entry(server_id) - .or_insert_with(Vec::new) - .push(project_path); - } - - if let Some((diagnostics_summary, (downstream_client, _))) = - diagnostics_summary.zip(lsp_store.downstream_client.as_ref()) - { - downstream_client.send(diagnostics_summary).log_err(); - } - for (server_id, paths) in updated_diagnostics_paths { - cx.emit(LspStoreEvent::DiagnosticsUpdated { server_id, paths }); + cx.emit(LspStoreEvent::DiagnosticsUpdated { + language_server_id: LanguageServerId(message.language_server_id as usize), + path: project_path, + }); } Ok(()) })? } async fn handle_start_language_server( - lsp_store: Entity, + this: Entity, envelope: TypedEnvelope, mut cx: AsyncApp, ) -> Result<()> { let server = envelope.payload.server.context("invalid server")?; - let server_capabilities = - serde_json::from_str::(&envelope.payload.capabilities) - .with_context(|| { - format!( - "incorrect server capabilities {}", - envelope.payload.capabilities - ) - })?; - lsp_store.update(&mut cx, |lsp_store, cx| { + + this.update(&mut cx, |this, cx| { let server_id = LanguageServerId(server.id as usize); - let server_name = LanguageServerName::from_proto(server.name.clone()); - lsp_store - .lsp_server_capabilities - .insert(server_id, server_capabilities); - lsp_store.language_server_statuses.insert( + this.language_server_statuses.insert( server_id, LanguageServerStatus { - name: server_name.clone(), + name: server.name.clone(), pending_work: Default::default(), has_pending_diagnostic_updates: false, progress_tokens: Default::default(), @@ -8896,7 +8765,7 @@ impl LspStore { ); cx.emit(LspStoreEvent::LanguageServerAdded( server_id, - server_name, + LanguageServerName(server.name.into()), server.worktree_id.map(WorktreeId::from_proto), )); cx.notify(); @@ -8957,8 +8826,7 @@ impl LspStore { } non_lsp @ proto::update_language_server::Variant::StatusUpdate(_) - | non_lsp @ proto::update_language_server::Variant::RegisteredForBuffer(_) - | non_lsp @ proto::update_language_server::Variant::MetadataUpdated(_) => { + | non_lsp @ proto::update_language_server::Variant::RegisteredForBuffer(_) => { cx.emit(LspStoreEvent::LanguageServerUpdate { language_server_id, name: envelope @@ -9001,10 +8869,10 @@ impl LspStore { async fn handle_lsp_ext_cancel_flycheck( lsp_store: Entity, envelope: TypedEnvelope, - cx: AsyncApp, + mut cx: AsyncApp, ) -> Result { let server_id = LanguageServerId(envelope.payload.language_server_id as usize); - lsp_store.read_with(&cx, |lsp_store, _| { + lsp_store.read_with(&mut cx, |lsp_store, _| { if let Some(server) = lsp_store.language_server_for_id(server_id) { server .notify::(&()) @@ -9026,22 +8894,13 @@ impl LspStore { lsp_store.update(&mut cx, |lsp_store, cx| { if let Some(server) = lsp_store.language_server_for_id(server_id) { let text_document = if envelope.payload.current_file_only { - let buffer_id = envelope - .payload - .buffer_id - .map(|id| BufferId::new(id)) - .transpose()?; - buffer_id - .and_then(|buffer_id| { - lsp_store - .buffer_store() - .read(cx) - .get(buffer_id) - .and_then(|buffer| { - Some(buffer.read(cx).file()?.as_local()?.abs_path(cx)) - }) - .map(|path| make_text_document_identifier(&path)) - }) + let buffer_id = BufferId::new(envelope.payload.buffer_id)?; + lsp_store + .buffer_store() + .read(cx) + .get(buffer_id) + .and_then(|buffer| Some(buffer.read(cx).file()?.as_local()?.abs_path(cx))) + .map(|path| make_text_document_identifier(&path)) .transpose()? } else { None @@ -9062,10 +8921,10 @@ impl LspStore { async fn handle_lsp_ext_clear_flycheck( lsp_store: Entity, envelope: TypedEnvelope, - cx: AsyncApp, + mut cx: AsyncApp, ) -> Result { let server_id = LanguageServerId(envelope.payload.language_server_id as usize); - lsp_store.read_with(&cx, |lsp_store, _| { + lsp_store.read_with(&mut cx, |lsp_store, _| { if let Some(server) = lsp_store.language_server_for_id(server_id) { server .notify::(&()) @@ -9228,7 +9087,7 @@ impl LspStore { new_path: &Path, is_dir: bool, cx: AsyncApp, - ) -> Task { + ) -> Task<()> { let old_uri = lsp::Url::from_file_path(old_path).ok().map(String::from); let new_uri = lsp::Url::from_file_path(new_path).ok().map(String::from); cx.spawn(async move |cx| { @@ -9244,7 +9103,11 @@ impl LspStore { else { continue; }; - + let Some(adapter) = + this.language_server_adapter_for_id(language_server.server_id()) + else { + continue; + }; if filter.should_send_will_rename(&old_uri, is_dir) { let apply_edit = cx.spawn({ let old_uri = old_uri.clone(); @@ -9261,16 +9124,17 @@ impl LspStore { .log_err() .flatten()?; - let transaction = LocalLspStore::deserialize_workspace_edit( + LocalLspStore::deserialize_workspace_edit( this.upgrade()?, edit, false, + adapter.clone(), language_server.clone(), cx, ) .await - .ok()?; - Some(transaction) + .ok(); + Some(()) } }); tasks.push(apply_edit); @@ -9280,17 +9144,11 @@ impl LspStore { }) .ok() .flatten(); - let mut merged_transaction = ProjectTransaction::default(); for task in tasks { // Await on tasks sequentially so that the order of application of edits is deterministic // (at least with regards to the order of registration of language servers) - if let Some(transaction) = task.await { - for (buffer, buffer_transaction) in transaction.0 { - merged_transaction.0.insert(buffer, buffer_transaction); - } - } + task.await; } - merged_transaction }) } @@ -9327,7 +9185,16 @@ impl LspStore { } pub fn language_server_for_id(&self, id: LanguageServerId) -> Option> { - self.as_local()?.language_server_for_id(id) + let local_lsp_store = self.as_local()?; + if let Some(LanguageServerState::Running { server, .. }) = + local_lsp_store.language_servers.get(&id) + { + Some(server.clone()) + } else if let Some((_, server)) = local_lsp_store.supplementary_language_servers.get(&id) { + Some(Arc::clone(server)) + } else { + None + } } fn on_lsp_progress( @@ -9391,7 +9258,9 @@ impl LspStore { let is_disk_based_diagnostics_progress = disk_based_diagnostics_progress_token .as_ref() - .is_some_and(|disk_based_token| token.starts_with(disk_based_token)); + .map_or(false, |disk_based_token| { + token.starts_with(disk_based_token) + }); match progress { lsp::WorkDoneProgress::Begin(report) => { @@ -9521,10 +9390,10 @@ impl LspStore { cx: &mut Context, ) { if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) { - if let Some(work) = status.pending_work.remove(&token) - && !work.is_disk_based_diagnostics_progress - { - cx.emit(LspStoreEvent::RefreshInlayHints); + if let Some(work) = status.pending_work.remove(&token) { + if !work.is_disk_based_diagnostics_progress { + cx.emit(LspStoreEvent::RefreshInlayHints); + } } cx.notify(); } @@ -9837,7 +9706,7 @@ impl LspStore { let peer_id = envelope.original_sender_id().unwrap_or_default(); let symbol = envelope.payload.symbol.context("invalid symbol")?; let symbol = Self::deserialize_symbol(symbol)?; - let symbol = this.read_with(&cx, |this, _| { + let symbol = this.read_with(&mut cx, |this, _| { let signature = this.symbol_signature(&symbol.path); anyhow::ensure!(signature == symbol.signature, "invalid symbol signature"); Ok(symbol) @@ -10087,7 +9956,7 @@ impl LspStore { ) -> Shared>>> { if let Some(environment) = &self.as_local().map(|local| local.environment.clone()) { environment.update(cx, |env, cx| { - env.get_buffer_environment(buffer, &self.worktree_store, cx) + env.get_buffer_environment(&buffer, &self.worktree_store, cx) }) } else { Task::ready(None).shared() @@ -10103,7 +9972,7 @@ impl LspStore { cx: &mut Context, ) -> Task> { let logger = zlog::scoped!("format"); - if self.as_local().is_some() { + if let Some(_) = self.as_local() { zlog::trace!(logger => "Formatting locally"); let logger = zlog::scoped!(logger => "local"); let buffers = buffers @@ -10318,10 +10187,10 @@ impl LspStore { None => None, }; - if let Some(server) = server - && let Some(shutdown) = server.shutdown() - { - shutdown.await; + if let Some(server) = server { + if let Some(shutdown) = server.shutdown() { + shutdown.await; + } } } @@ -10331,18 +10200,28 @@ impl LspStore { &mut self, server_id: LanguageServerId, cx: &mut Context, - ) -> Task<()> { + ) -> Task> { let local = match &mut self.mode { LspStoreMode::Local(local) => local, _ => { - return Task::ready(()); + return Task::ready(Vec::new()); } }; + let mut orphaned_worktrees = Vec::new(); // Remove this server ID from all entries in the given worktree. - local - .language_server_ids - .retain(|_, state| state.id != server_id); + local.language_server_ids.retain(|(worktree, _), ids| { + if !ids.remove(&server_id) { + return true; + } + + if ids.is_empty() { + orphaned_worktrees.push(*worktree); + false + } else { + true + } + }); self.buffer_store.update(cx, |buffer_store, cx| { for buffer in buffer_store.buffers() { buffer.update(cx, |buffer, cx| { @@ -10366,7 +10245,6 @@ impl LspStore { error_count: 0, warning_count: 0, }), - more_summaries: Vec::new(), }) .log_err(); } @@ -10395,7 +10273,7 @@ impl LspStore { let name = self .language_server_statuses .remove(&server_id) - .map(|status| status.name) + .map(|status| LanguageServerName::from(status.name.as_str())) .or_else(|| { if let Some(LanguageServerState::Running { adapter, .. }) = server_state.as_ref() { Some(adapter.name()) @@ -10421,13 +10299,14 @@ impl LspStore { cx.notify(); }) .ok(); + orphaned_worktrees }); } if server_state.is_some() { cx.emit(LspStoreEvent::LanguageServerRemoved(server_id)); } - Task::ready(()) + Task::ready(orphaned_worktrees) } pub fn stop_all_language_servers(&mut self, cx: &mut Context) { @@ -10446,9 +10325,12 @@ impl LspStore { let language_servers_to_stop = local .language_server_ids .values() - .map(|state| state.id) + .flatten() + .copied() .collect(); - local.lsp_tree.remove_nodes(&language_servers_to_stop); + local.lsp_tree.update(cx, |this, _| { + this.remove_nodes(&language_servers_to_stop); + }); let tasks = language_servers_to_stop .into_iter() .map(|server| self.stop_local_language_server(server, cx)) @@ -10595,31 +10477,37 @@ impl LspStore { for buffer in buffers { buffer.update(cx, |buffer, cx| { language_servers_to_stop.extend(local.language_server_ids_for_buffer(buffer, cx)); - if let Some(worktree_id) = buffer.file().map(|f| f.worktree_id(cx)) - && covered_worktrees.insert(worktree_id) - { - language_server_names_to_stop.retain(|name| { - let old_ids_count = language_servers_to_stop.len(); - let all_language_servers_with_this_name = local - .language_server_ids - .iter() - .filter_map(|(seed, state)| seed.name.eq(name).then(|| state.id)); - language_servers_to_stop.extend(all_language_servers_with_this_name); - old_ids_count == language_servers_to_stop.len() - }); + if let Some(worktree_id) = buffer.file().map(|f| f.worktree_id(cx)) { + if covered_worktrees.insert(worktree_id) { + language_server_names_to_stop.retain(|name| { + match local.language_server_ids.get(&(worktree_id, name.clone())) { + Some(server_ids) => { + language_servers_to_stop + .extend(server_ids.into_iter().copied()); + false + } + None => true, + } + }); + } } }); } for name in language_server_names_to_stop { - language_servers_to_stop.extend( - local - .language_server_ids - .iter() - .filter_map(|(seed, v)| seed.name.eq(&name).then(|| v.id)), - ); + if let Some(server_ids) = local + .language_server_ids + .iter() + .filter(|((_, server_name), _)| server_name == &name) + .map(|((_, _), server_ids)| server_ids) + .max_by_key(|server_ids| server_ids.len()) + { + language_servers_to_stop.extend(server_ids.into_iter().copied()); + } } - local.lsp_tree.remove_nodes(&language_servers_to_stop); + local.lsp_tree.update(cx, |this, _| { + this.remove_nodes(&language_servers_to_stop); + }); let tasks = language_servers_to_stop .into_iter() .map(|server| self.stop_local_language_server(server, cx)) @@ -10645,86 +10533,58 @@ impl LspStore { ) } - #[cfg(any(test, feature = "test-support"))] pub fn update_diagnostics( &mut self, - server_id: LanguageServerId, - diagnostics: lsp::PublishDiagnosticsParams, + language_server_id: LanguageServerId, + params: lsp::PublishDiagnosticsParams, result_id: Option, source_kind: DiagnosticSourceKind, disk_based_sources: &[String], cx: &mut Context, ) -> Result<()> { - self.merge_lsp_diagnostics( + self.merge_diagnostics( + language_server_id, + params, + result_id, source_kind, - vec![DocumentDiagnosticsUpdate { - diagnostics, - result_id, - server_id, - disk_based_sources: Cow::Borrowed(disk_based_sources), - }], + disk_based_sources, |_, _, _| false, cx, ) } - pub fn merge_lsp_diagnostics( + pub fn merge_diagnostics( &mut self, + language_server_id: LanguageServerId, + mut params: lsp::PublishDiagnosticsParams, + result_id: Option, source_kind: DiagnosticSourceKind, - lsp_diagnostics: Vec>, - merge: impl Fn(&Buffer, &Diagnostic, &App) -> bool + Clone, + disk_based_sources: &[String], + filter: impl Fn(&Buffer, &Diagnostic, &App) -> bool + Clone, cx: &mut Context, ) -> Result<()> { anyhow::ensure!(self.mode.is_local(), "called update_diagnostics on remote"); - let updates = lsp_diagnostics - .into_iter() - .filter_map(|update| { - let abs_path = update.diagnostics.uri.to_file_path().ok()?; - Some(DocumentDiagnosticsUpdate { - diagnostics: self.lsp_to_document_diagnostics( - abs_path, - source_kind, - update.server_id, - update.diagnostics, - &update.disk_based_sources, - ), - result_id: update.result_id, - server_id: update.server_id, - disk_based_sources: update.disk_based_sources, - }) - }) - .collect(); - self.merge_diagnostic_entries(updates, merge, cx)?; - Ok(()) - } - - fn lsp_to_document_diagnostics( - &mut self, - document_abs_path: PathBuf, - source_kind: DiagnosticSourceKind, - server_id: LanguageServerId, - mut lsp_diagnostics: lsp::PublishDiagnosticsParams, - disk_based_sources: &[String], - ) -> DocumentDiagnostics { + let abs_path = params + .uri + .to_file_path() + .map_err(|()| anyhow!("URI is not a file"))?; let mut diagnostics = Vec::default(); let mut primary_diagnostic_group_ids = HashMap::default(); let mut sources_by_group_id = HashMap::default(); let mut supporting_diagnostics = HashMap::default(); - let adapter = self.language_server_adapter_for_id(server_id); + let adapter = self.language_server_adapter_for_id(language_server_id); // Ensure that primary diagnostics are always the most severe - lsp_diagnostics - .diagnostics - .sort_by_key(|item| item.severity); + params.diagnostics.sort_by_key(|item| item.severity); - for diagnostic in &lsp_diagnostics.diagnostics { + for diagnostic in ¶ms.diagnostics { let source = diagnostic.source.as_ref(); let range = range_from_lsp(diagnostic.range); let is_supporting = diagnostic .related_information .as_ref() - .is_some_and(|infos| { + .map_or(false, |infos| { infos.iter().any(|info| { primary_diagnostic_group_ids.contains_key(&( source, @@ -10737,11 +10597,11 @@ impl LspStore { let is_unnecessary = diagnostic .tags .as_ref() - .is_some_and(|tags| tags.contains(&DiagnosticTag::UNNECESSARY)); + .map_or(false, |tags| tags.contains(&DiagnosticTag::UNNECESSARY)); let underline = self - .language_server_adapter_for_id(server_id) - .is_none_or(|adapter| adapter.underline_diagnostic(diagnostic)); + .language_server_adapter_for_id(language_server_id) + .map_or(true, |adapter| adapter.underline_diagnostic(diagnostic)); if is_supporting { supporting_diagnostics.insert( @@ -10751,7 +10611,7 @@ impl LspStore { } else { let group_id = post_inc(&mut self.as_local_mut().unwrap().next_diagnostic_group_id); let is_disk_based = - source.is_some_and(|source| disk_based_sources.contains(source)); + source.map_or(false, |source| disk_based_sources.contains(source)); sources_by_group_id.insert(group_id, source); primary_diagnostic_group_ids @@ -10782,7 +10642,7 @@ impl LspStore { }); if let Some(infos) = &diagnostic.related_information { for info in infos { - if info.location.uri == lsp_diagnostics.uri && !info.message.is_empty() { + if info.location.uri == params.uri && !info.message.is_empty() { let range = range_from_lsp(info.location.range); diagnostics.push(DiagnosticEntry { range, @@ -10830,11 +10690,16 @@ impl LspStore { } } - DocumentDiagnostics { + self.merge_diagnostic_entries( + language_server_id, + abs_path, + result_id, + params.version, diagnostics, - document_abs_path, - version: lsp_diagnostics.version, - } + filter, + cx, + )?; + Ok(()) } fn insert_newly_running_language_server( @@ -10842,7 +10707,7 @@ impl LspStore { adapter: Arc, language_server: Arc, server_id: LanguageServerId, - key: LanguageServerSeed, + key: (WorktreeId, LanguageServerName), workspace_folders: Arc>>, cx: &mut Context, ) { @@ -10854,7 +10719,7 @@ impl LspStore { if local .language_server_ids .get(&key) - .map(|state| state.id != server_id) + .map(|ids| !ids.contains(&server_id)) .unwrap_or(false) { return; @@ -10901,7 +10766,7 @@ impl LspStore { self.language_server_statuses.insert( server_id, LanguageServerStatus { - name: language_server.name(), + name: language_server.name().to_string(), pending_work: Default::default(), has_pending_diagnostic_updates: false, progress_tokens: Default::default(), @@ -10911,37 +10776,32 @@ impl LspStore { cx.emit(LspStoreEvent::LanguageServerAdded( server_id, language_server.name(), - Some(key.worktree_id), + Some(key.0), )); cx.emit(LspStoreEvent::RefreshInlayHints); - let server_capabilities = language_server.capabilities(); if let Some((downstream_client, project_id)) = self.downstream_client.as_ref() { downstream_client .send(proto::StartLanguageServer { project_id: *project_id, server: Some(proto::LanguageServer { - id: server_id.to_proto(), + id: server_id.0 as u64, name: language_server.name().to_string(), - worktree_id: Some(key.worktree_id.to_proto()), + worktree_id: Some(key.0.to_proto()), }), - capabilities: serde_json::to_string(&server_capabilities) - .expect("serializing server LSP capabilities"), }) .log_err(); } - self.lsp_server_capabilities - .insert(server_id, server_capabilities); // Tell the language server about every open buffer in the worktree that matches the language. // Also check for buffers in worktrees that reused this server - let mut worktrees_using_server = vec![key.worktree_id]; + let mut worktrees_using_server = vec![key.0]; if let Some(local) = self.as_local() { // Find all worktrees that have this server in their language server tree - for (worktree_id, servers) in &local.lsp_tree.instances { - if *worktree_id != key.worktree_id { - for server_map in servers.roots.values() { - if server_map.contains_key(&key.name) { + for (worktree_id, servers) in &local.lsp_tree.read(cx).instances { + if *worktree_id != key.0 { + for (_, server_map) in &servers.roots { + if server_map.contains_key(&key.1) { worktrees_using_server.push(*worktree_id); } } @@ -10967,7 +10827,7 @@ impl LspStore { .languages .lsp_adapters(&language.name()) .iter() - .any(|a| a.name == key.name) + .any(|a| a.name == key.1) { continue; } @@ -10979,11 +10839,10 @@ impl LspStore { let local = self.as_local_mut().unwrap(); - let buffer_id = buffer.remote_id(); - if local.registered_buffers.contains_key(&buffer_id) { + if local.registered_buffers.contains_key(&buffer.remote_id()) { let versions = local .buffer_snapshots - .entry(buffer_id) + .entry(buffer.remote_id()) .or_default() .entry(server_id) .and_modify(|_| { @@ -11009,10 +10868,10 @@ impl LspStore { version, initial_snapshot.text(), ); - buffer_paths_registered.push((buffer_id, file.abs_path(cx))); + buffer_paths_registered.push(file.abs_path(cx)); local .buffers_opened_in_servers - .entry(buffer_id) + .entry(buffer.remote_id()) .or_default() .insert(server_id); } @@ -11036,14 +10895,13 @@ impl LspStore { } }); - for (buffer_id, abs_path) in buffer_paths_registered { + for abs_path in buffer_paths_registered { cx.emit(LspStoreEvent::LanguageServerUpdate { language_server_id: server_id, name: Some(adapter.name()), message: proto::update_language_server::Variant::RegisteredForBuffer( proto::RegisteredForBuffer { buffer_abs_path: abs_path.to_string_lossy().to_string(), - buffer_id: buffer_id.to_proto(), }, ), }); @@ -11111,10 +10969,10 @@ impl LspStore { if let Some((LanguageServerState::Running { server, .. }, status)) = server.zip(status) { for (token, progress) in &status.pending_work { - if let Some(token_to_cancel) = token_to_cancel.as_ref() - && token != token_to_cancel - { - continue; + if let Some(token_to_cancel) = token_to_cancel.as_ref() { + if token != token_to_cancel { + continue; + } } if progress.is_cancellable { server @@ -11205,14 +11063,18 @@ impl LspStore { let Some(local) = self.as_local() else { return }; local.prettier_store.update(cx, |prettier_store, cx| { - prettier_store.update_prettier_settings(worktree_handle, changes, cx) + prettier_store.update_prettier_settings(&worktree_handle, changes, cx) }); let worktree_id = worktree_handle.read(cx).id(); let mut language_server_ids = local .language_server_ids .iter() - .filter_map(|(seed, v)| seed.worktree_id.eq(&worktree_id).then(|| v.id)) + .flat_map(|((server_worktree, _), server_ids)| { + server_ids + .iter() + .filter_map(|server_id| server_worktree.eq(&worktree_id).then(|| *server_id)) + }) .collect::>(); language_server_ids.sort(); language_server_ids.dedup(); @@ -11221,47 +11083,41 @@ impl LspStore { for server_id in &language_server_ids { if let Some(LanguageServerState::Running { server, .. }) = local.language_servers.get(server_id) - && let Some(watched_paths) = local + { + if let Some(watched_paths) = local .language_server_watched_paths .get(server_id) .and_then(|paths| paths.worktree_paths.get(&worktree_id)) - { - let params = lsp::DidChangeWatchedFilesParams { - changes: changes - .iter() - .filter_map(|(path, _, change)| { - if !watched_paths.is_match(path) { - return None; - } - let typ = match change { - PathChange::Loaded => return None, - PathChange::Added => lsp::FileChangeType::CREATED, - PathChange::Removed => lsp::FileChangeType::DELETED, - PathChange::Updated => lsp::FileChangeType::CHANGED, - PathChange::AddedOrUpdated => lsp::FileChangeType::CHANGED, - }; - Some(lsp::FileEvent { - uri: lsp::Url::from_file_path(abs_path.join(path)).unwrap(), - typ, + { + let params = lsp::DidChangeWatchedFilesParams { + changes: changes + .iter() + .filter_map(|(path, _, change)| { + if !watched_paths.is_match(path) { + return None; + } + let typ = match change { + PathChange::Loaded => return None, + PathChange::Added => lsp::FileChangeType::CREATED, + PathChange::Removed => lsp::FileChangeType::DELETED, + PathChange::Updated => lsp::FileChangeType::CHANGED, + PathChange::AddedOrUpdated => lsp::FileChangeType::CHANGED, + }; + Some(lsp::FileEvent { + uri: lsp::Url::from_file_path(abs_path.join(path)).unwrap(), + typ, + }) }) - }) - .collect(), - }; - if !params.changes.is_empty() { - server - .notify::(¶ms) - .ok(); + .collect(), + }; + if !params.changes.is_empty() { + server + .notify::(¶ms) + .ok(); + } } } } - for (path, _, _) in changes { - if let Some(file_name) = path.file_name().and_then(|file_name| file_name.to_str()) - && local.watched_manifest_filenames.contains(file_name) - { - self.request_workspace_config_refresh(); - break; - } - } } pub fn wait_for_remote_buffer( @@ -11503,7 +11359,6 @@ impl LspStore { } fn cleanup_lsp_data(&mut self, for_server: LanguageServerId) { - self.lsp_server_capabilities.remove(&for_server); for buffer_colors in self.lsp_document_colors.values_mut() { buffer_colors.colors.remove(&for_server); buffer_colors.cache_version += 1; @@ -11592,593 +11447,69 @@ impl LspStore { ) { let workspace_diagnostics = GetDocumentDiagnostics::deserialize_workspace_diagnostics_report(report, server_id); - let mut unchanged_buffers = HashSet::default(); - let mut changed_buffers = HashSet::default(); - let workspace_diagnostics_updates = workspace_diagnostics - .into_iter() - .filter_map( - |workspace_diagnostics| match workspace_diagnostics.diagnostics { - LspPullDiagnostics::Response { + for workspace_diagnostics in workspace_diagnostics { + let LspPullDiagnostics::Response { + server_id, + uri, + diagnostics, + } = workspace_diagnostics.diagnostics + else { + continue; + }; + + let adapter = self.language_server_adapter_for_id(server_id); + let disk_based_sources = adapter + .as_ref() + .map(|adapter| adapter.disk_based_diagnostic_sources.as_slice()) + .unwrap_or(&[]); + + match diagnostics { + PulledDiagnostics::Unchanged { result_id } => { + self.merge_diagnostics( server_id, - uri, - diagnostics, - } => Some((server_id, uri, diagnostics, workspace_diagnostics.version)), - LspPullDiagnostics::Default => None, - }, - ) - .fold( - HashMap::default(), - |mut acc, (server_id, uri, diagnostics, version)| { - let (result_id, diagnostics) = match diagnostics { - PulledDiagnostics::Unchanged { result_id } => { - unchanged_buffers.insert(uri.clone()); - (Some(result_id), Vec::new()) - } - PulledDiagnostics::Changed { - result_id, + lsp::PublishDiagnosticsParams { + uri: uri.clone(), + diagnostics: Vec::new(), + version: None, + }, + Some(result_id), + DiagnosticSourceKind::Pulled, + disk_based_sources, + |_, _, _| true, + cx, + ) + .log_err(); + } + PulledDiagnostics::Changed { + diagnostics, + result_id, + } => { + self.merge_diagnostics( + server_id, + lsp::PublishDiagnosticsParams { + uri: uri.clone(), diagnostics, - } => { - changed_buffers.insert(uri.clone()); - (result_id, diagnostics) - } - }; - let disk_based_sources = Cow::Owned( - self.language_server_adapter_for_id(server_id) - .as_ref() - .map(|adapter| adapter.disk_based_diagnostic_sources.as_slice()) - .unwrap_or(&[]) - .to_vec(), - ); - acc.entry(server_id) - .or_insert_with(Vec::new) - .push(DocumentDiagnosticsUpdate { - server_id, - diagnostics: lsp::PublishDiagnosticsParams { - uri, - diagnostics, - version, - }, - result_id, - disk_based_sources, - }); - acc - }, - ); - - for diagnostic_updates in workspace_diagnostics_updates.into_values() { - self.merge_lsp_diagnostics( - DiagnosticSourceKind::Pulled, - diagnostic_updates, - |buffer, old_diagnostic, cx| { - File::from_dyn(buffer.file()) - .and_then(|file| { - let abs_path = file.as_local()?.abs_path(cx); - lsp::Url::from_file_path(abs_path).ok() - }) - .is_none_or(|buffer_uri| { - unchanged_buffers.contains(&buffer_uri) - || match old_diagnostic.source_kind { - DiagnosticSourceKind::Pulled => { - !changed_buffers.contains(&buffer_uri) - } - DiagnosticSourceKind::Other | DiagnosticSourceKind::Pushed => { - true - } - } - }) - }, - cx, - ) - .log_err(); - } - } - - fn register_server_capabilities( - &mut self, - server_id: LanguageServerId, - params: lsp::RegistrationParams, - cx: &mut Context, - ) -> anyhow::Result<()> { - let server = self - .language_server_for_id(server_id) - .with_context(|| format!("no server {server_id} found"))?; - for reg in params.registrations { - match reg.method.as_str() { - "workspace/didChangeWatchedFiles" => { - if let Some(options) = reg.register_options { - let notify = if let Some(local_lsp_store) = self.as_local_mut() { - let caps = serde_json::from_value(options)?; - local_lsp_store - .on_lsp_did_change_watched_files(server_id, ®.id, caps, cx); - true - } else { - false - }; - if notify { - notify_server_capabilities_updated(&server, cx); - } - } - } - "workspace/didChangeConfiguration" => { - // Ignore payload since we notify clients of setting changes unconditionally, relying on them pulling the latest settings. - } - "workspace/symbol" => { - let options = parse_register_capabilities(reg)?; - server.update_capabilities(|capabilities| { - capabilities.workspace_symbol_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); - } - "workspace/fileOperations" => { - if let Some(options) = reg.register_options { - let caps = serde_json::from_value(options)?; - server.update_capabilities(|capabilities| { - capabilities - .workspace - .get_or_insert_default() - .file_operations = Some(caps); - }); - notify_server_capabilities_updated(&server, cx); - } - } - "workspace/executeCommand" => { - if let Some(options) = reg.register_options { - let options = serde_json::from_value(options)?; - server.update_capabilities(|capabilities| { - capabilities.execute_command_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); - } - } - "textDocument/rangeFormatting" => { - let options = parse_register_capabilities(reg)?; - server.update_capabilities(|capabilities| { - capabilities.document_range_formatting_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); - } - "textDocument/onTypeFormatting" => { - if let Some(options) = reg - .register_options - .map(serde_json::from_value) - .transpose()? - { - server.update_capabilities(|capabilities| { - capabilities.document_on_type_formatting_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); - } - } - "textDocument/formatting" => { - let options = parse_register_capabilities(reg)?; - server.update_capabilities(|capabilities| { - capabilities.document_formatting_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); - } - "textDocument/rename" => { - let options = parse_register_capabilities(reg)?; - server.update_capabilities(|capabilities| { - capabilities.rename_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); - } - "textDocument/inlayHint" => { - let options = parse_register_capabilities(reg)?; - server.update_capabilities(|capabilities| { - capabilities.inlay_hint_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); - } - "textDocument/documentSymbol" => { - let options = parse_register_capabilities(reg)?; - server.update_capabilities(|capabilities| { - capabilities.document_symbol_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); - } - "textDocument/codeAction" => { - if let Some(options) = reg - .register_options - .map(serde_json::from_value) - .transpose()? - { - server.update_capabilities(|capabilities| { - capabilities.code_action_provider = - Some(lsp::CodeActionProviderCapability::Options(options)); - }); - notify_server_capabilities_updated(&server, cx); - } - } - "textDocument/definition" => { - let options = parse_register_capabilities(reg)?; - server.update_capabilities(|capabilities| { - capabilities.definition_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); - } - "textDocument/completion" => { - if let Some(caps) = reg - .register_options - .map(serde_json::from_value) - .transpose()? - { - server.update_capabilities(|capabilities| { - capabilities.completion_provider = Some(caps); - }); - notify_server_capabilities_updated(&server, cx); - } - } - "textDocument/hover" => { - if let Some(caps) = reg - .register_options - .map(serde_json::from_value) - .transpose()? - { - server.update_capabilities(|capabilities| { - capabilities.hover_provider = Some(caps); - }); - notify_server_capabilities_updated(&server, cx); - } - } - "textDocument/signatureHelp" => { - if let Some(caps) = reg - .register_options - .map(serde_json::from_value) - .transpose()? - { - server.update_capabilities(|capabilities| { - capabilities.signature_help_provider = Some(caps); - }); - notify_server_capabilities_updated(&server, cx); - } - } - "textDocument/didChange" => { - if let Some(sync_kind) = reg - .register_options - .and_then(|opts| opts.get("syncKind").cloned()) - .map(serde_json::from_value::) - .transpose()? - { - server.update_capabilities(|capabilities| { - let mut sync_options = - Self::take_text_document_sync_options(capabilities); - sync_options.change = Some(sync_kind); - capabilities.text_document_sync = - Some(lsp::TextDocumentSyncCapability::Options(sync_options)); - }); - notify_server_capabilities_updated(&server, cx); - } - } - "textDocument/didSave" => { - if let Some(include_text) = reg - .register_options - .map(|opts| { - let transpose = opts - .get("includeText") - .cloned() - .map(serde_json::from_value::>) - .transpose(); - match transpose { - Ok(value) => Ok(value.flatten()), - Err(e) => Err(e), + version: workspace_diagnostics.version, + }, + result_id, + DiagnosticSourceKind::Pulled, + disk_based_sources, + |buffer, old_diagnostic, cx| match old_diagnostic.source_kind { + DiagnosticSourceKind::Pulled => { + let buffer_url = File::from_dyn(buffer.file()) + .map(|f| f.abs_path(cx)) + .and_then(|abs_path| file_path_to_lsp_url(&abs_path).ok()); + buffer_url.is_none_or(|buffer_url| buffer_url != uri) } - }) - .transpose()? - { - server.update_capabilities(|capabilities| { - let mut sync_options = - Self::take_text_document_sync_options(capabilities); - sync_options.save = - Some(TextDocumentSyncSaveOptions::SaveOptions(lsp::SaveOptions { - include_text, - })); - capabilities.text_document_sync = - Some(lsp::TextDocumentSyncCapability::Options(sync_options)); - }); - notify_server_capabilities_updated(&server, cx); - } + DiagnosticSourceKind::Other | DiagnosticSourceKind::Pushed => true, + }, + cx, + ) + .log_err(); } - "textDocument/codeLens" => { - if let Some(caps) = reg - .register_options - .map(serde_json::from_value) - .transpose()? - { - server.update_capabilities(|capabilities| { - capabilities.code_lens_provider = Some(caps); - }); - notify_server_capabilities_updated(&server, cx); - } - } - "textDocument/diagnostic" => { - if let Some(caps) = reg - .register_options - .map(serde_json::from_value) - .transpose()? - { - server.update_capabilities(|capabilities| { - capabilities.diagnostic_provider = Some(caps); - }); - notify_server_capabilities_updated(&server, cx); - } - } - "textDocument/documentColor" => { - if let Some(caps) = reg - .register_options - .map(serde_json::from_value) - .transpose()? - { - server.update_capabilities(|capabilities| { - capabilities.color_provider = Some(caps); - }); - notify_server_capabilities_updated(&server, cx); - } - } - _ => log::warn!("unhandled capability registration: {reg:?}"), } } - - Ok(()) } - - fn unregister_server_capabilities( - &mut self, - server_id: LanguageServerId, - params: lsp::UnregistrationParams, - cx: &mut Context, - ) -> anyhow::Result<()> { - let server = self - .language_server_for_id(server_id) - .with_context(|| format!("no server {server_id} found"))?; - for unreg in params.unregisterations.iter() { - match unreg.method.as_str() { - "workspace/didChangeWatchedFiles" => { - let notify = if let Some(local_lsp_store) = self.as_local_mut() { - local_lsp_store - .on_lsp_unregister_did_change_watched_files(server_id, &unreg.id, cx); - true - } else { - false - }; - if notify { - notify_server_capabilities_updated(&server, cx); - } - } - "workspace/didChangeConfiguration" => { - // Ignore payload since we notify clients of setting changes unconditionally, relying on them pulling the latest settings. - } - "workspace/symbol" => { - server.update_capabilities(|capabilities| { - capabilities.workspace_symbol_provider = None - }); - notify_server_capabilities_updated(&server, cx); - } - "workspace/fileOperations" => { - server.update_capabilities(|capabilities| { - capabilities - .workspace - .get_or_insert_with(|| lsp::WorkspaceServerCapabilities { - workspace_folders: None, - file_operations: None, - }) - .file_operations = None; - }); - notify_server_capabilities_updated(&server, cx); - } - "workspace/executeCommand" => { - server.update_capabilities(|capabilities| { - capabilities.execute_command_provider = None; - }); - notify_server_capabilities_updated(&server, cx); - } - "textDocument/rangeFormatting" => { - server.update_capabilities(|capabilities| { - capabilities.document_range_formatting_provider = None - }); - notify_server_capabilities_updated(&server, cx); - } - "textDocument/onTypeFormatting" => { - server.update_capabilities(|capabilities| { - capabilities.document_on_type_formatting_provider = None; - }); - notify_server_capabilities_updated(&server, cx); - } - "textDocument/formatting" => { - server.update_capabilities(|capabilities| { - capabilities.document_formatting_provider = None; - }); - notify_server_capabilities_updated(&server, cx); - } - "textDocument/rename" => { - server.update_capabilities(|capabilities| capabilities.rename_provider = None); - notify_server_capabilities_updated(&server, cx); - } - "textDocument/codeAction" => { - server.update_capabilities(|capabilities| { - capabilities.code_action_provider = None; - }); - notify_server_capabilities_updated(&server, cx); - } - "textDocument/definition" => { - server.update_capabilities(|capabilities| { - capabilities.definition_provider = None; - }); - notify_server_capabilities_updated(&server, cx); - } - "textDocument/completion" => { - server.update_capabilities(|capabilities| { - capabilities.completion_provider = None; - }); - notify_server_capabilities_updated(&server, cx); - } - "textDocument/hover" => { - server.update_capabilities(|capabilities| { - capabilities.hover_provider = None; - }); - notify_server_capabilities_updated(&server, cx); - } - "textDocument/signatureHelp" => { - server.update_capabilities(|capabilities| { - capabilities.signature_help_provider = None; - }); - notify_server_capabilities_updated(&server, cx); - } - "textDocument/didChange" => { - server.update_capabilities(|capabilities| { - let mut sync_options = Self::take_text_document_sync_options(capabilities); - sync_options.change = None; - capabilities.text_document_sync = - Some(lsp::TextDocumentSyncCapability::Options(sync_options)); - }); - notify_server_capabilities_updated(&server, cx); - } - "textDocument/didSave" => { - server.update_capabilities(|capabilities| { - let mut sync_options = Self::take_text_document_sync_options(capabilities); - sync_options.save = None; - capabilities.text_document_sync = - Some(lsp::TextDocumentSyncCapability::Options(sync_options)); - }); - notify_server_capabilities_updated(&server, cx); - } - "textDocument/codeLens" => { - server.update_capabilities(|capabilities| { - capabilities.code_lens_provider = None; - }); - notify_server_capabilities_updated(&server, cx); - } - "textDocument/diagnostic" => { - server.update_capabilities(|capabilities| { - capabilities.diagnostic_provider = None; - }); - notify_server_capabilities_updated(&server, cx); - } - "textDocument/documentColor" => { - server.update_capabilities(|capabilities| { - capabilities.color_provider = None; - }); - notify_server_capabilities_updated(&server, cx); - } - _ => log::warn!("unhandled capability unregistration: {unreg:?}"), - } - } - - Ok(()) - } - - async fn query_lsp_locally( - lsp_store: Entity, - sender_id: proto::PeerId, - lsp_request_id: LspRequestId, - proto_request: T::ProtoRequest, - position: Option, - mut cx: AsyncApp, - ) -> Result<()> - where - T: LspCommand + Clone, - T::ProtoRequest: proto::LspRequestMessage, - ::Response: - Into<::Response>, - { - let buffer_id = BufferId::new(proto_request.buffer_id())?; - let version = deserialize_version(proto_request.buffer_version()); - let buffer = lsp_store.update(&mut cx, |this, cx| { - this.buffer_store.read(cx).get_existing(buffer_id) - })??; - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(version.clone()) - })? - .await?; - let buffer_version = buffer.read_with(&cx, |buffer, _| buffer.version())?; - let request = - T::from_proto(proto_request, lsp_store.clone(), buffer.clone(), cx.clone()).await?; - lsp_store.update(&mut cx, |lsp_store, cx| { - let request_task = - lsp_store.request_multiple_lsp_locally(&buffer, position, request, cx); - let existing_queries = lsp_store - .running_lsp_requests - .entry(TypeId::of::()) - .or_default(); - if T::ProtoRequest::stop_previous_requests() - || buffer_version.changed_since(&existing_queries.0) - { - existing_queries.1.clear(); - } - existing_queries.1.insert( - lsp_request_id, - cx.spawn(async move |lsp_store, cx| { - let response = request_task.await; - lsp_store - .update(cx, |lsp_store, cx| { - if let Some((client, project_id)) = lsp_store.downstream_client.clone() - { - let response = response - .into_iter() - .map(|(server_id, response)| { - ( - server_id.to_proto(), - T::response_to_proto( - response, - lsp_store, - sender_id, - &buffer_version, - cx, - ) - .into(), - ) - }) - .collect::>(); - match client.send_lsp_response::( - project_id, - lsp_request_id, - response, - ) { - Ok(()) => {} - Err(e) => { - log::error!("Failed to send LSP response: {e:#}",) - } - } - } - }) - .ok(); - }), - ); - })?; - Ok(()) - } - - fn take_text_document_sync_options( - capabilities: &mut lsp::ServerCapabilities, - ) -> lsp::TextDocumentSyncOptions { - match capabilities.text_document_sync.take() { - Some(lsp::TextDocumentSyncCapability::Options(sync_options)) => sync_options, - Some(lsp::TextDocumentSyncCapability::Kind(sync_kind)) => { - let mut sync_options = lsp::TextDocumentSyncOptions::default(); - sync_options.change = Some(sync_kind); - sync_options - } - None => lsp::TextDocumentSyncOptions::default(), - } - } - - #[cfg(any(test, feature = "test-support"))] - pub fn forget_code_lens_task(&mut self, buffer_id: BufferId) -> Option { - let data = self.lsp_code_lens.get_mut(&buffer_id)?; - Some(data.update.take()?.1) - } -} - -// Registration with registerOptions as null, should fallback to true. -// https://github.com/microsoft/vscode-languageserver-node/blob/d90a87f9557a0df9142cfb33e251cfa6fe27d970/client/src/common/client.ts#L2133 -fn parse_register_capabilities( - reg: lsp::Registration, -) -> Result> { - Ok(match reg.register_options { - Some(options) => OneOf::Right(serde_json::from_value::(options)?), - None => OneOf::Left(true), - }) } fn subscribe_to_binary_statuses( @@ -12413,10 +11744,11 @@ async fn populate_labels_for_completions( let lsp_completions = new_completions .iter() .filter_map(|new_completion| { - new_completion - .source - .lsp_completion(true) - .map(|lsp_completion| lsp_completion.into_owned()) + if let Some(lsp_completion) = new_completion.source.lsp_completion(true) { + Some(lsp_completion.into_owned()) + } else { + None + } }) .collect::>(); @@ -12436,7 +11768,11 @@ async fn populate_labels_for_completions( for completion in new_completions { match completion.source.lsp_completion(true) { Some(lsp_completion) => { - let documentation = lsp_completion.documentation.clone().map(|docs| docs.into()); + let documentation = if let Some(docs) = lsp_completion.documentation.clone() { + Some(docs.into()) + } else { + None + }; let mut label = labels.next().flatten().unwrap_or_else(|| { CodeLabel::fallback_for_completion(&lsp_completion, language.as_deref()) @@ -12532,7 +11868,7 @@ impl TryFrom<&FileOperationFilter> for RenameActionPredicate { ops.pattern .options .as_ref() - .is_some_and(|ops| ops.ignore_case.unwrap_or(false)), + .map_or(false, |ops| ops.ignore_case.unwrap_or(false)), ) .build()? .compile_matcher(), @@ -12547,7 +11883,7 @@ struct RenameActionPredicate { impl RenameActionPredicate { // Returns true if language server should be notified fn eval(&self, path: &str, is_dir: bool) -> bool { - self.kind.as_ref().is_none_or(|kind| { + self.kind.as_ref().map_or(true, |kind| { let expected_kind = if is_dir { FileOperationPatternKind::Folder } else { @@ -12824,7 +12160,7 @@ impl DiagnosticSummary { } pub fn to_proto( - self, + &self, language_server_id: LanguageServerId, path: &Path, ) -> proto::DiagnosticSummary { @@ -12941,7 +12277,7 @@ impl LspAdapter for SshLspAdapter { async fn check_if_user_installed( &self, _: &dyn LspAdapterDelegate, - _: Option, + _: Arc, _: &AsyncApp, ) -> Option { Some(self.binary.clone()) @@ -13264,18 +12600,24 @@ async fn populate_labels_for_symbols( fn include_text(server: &lsp::LanguageServer) -> Option { match server.capabilities().text_document_sync.as_ref()? { - lsp::TextDocumentSyncCapability::Options(opts) => match opts.save.as_ref()? { - // Server wants didSave but didn't specify includeText. - lsp::TextDocumentSyncSaveOptions::Supported(true) => Some(false), - // Server doesn't want didSave at all. - lsp::TextDocumentSyncSaveOptions::Supported(false) => None, - // Server provided SaveOptions. + lsp::TextDocumentSyncCapability::Kind(kind) => match *kind { + lsp::TextDocumentSyncKind::NONE => None, + lsp::TextDocumentSyncKind::FULL => Some(true), + lsp::TextDocumentSyncKind::INCREMENTAL => Some(false), + _ => None, + }, + lsp::TextDocumentSyncCapability::Options(options) => match options.save.as_ref()? { + lsp::TextDocumentSyncSaveOptions::Supported(supported) => { + if *supported { + Some(true) + } else { + None + } + } lsp::TextDocumentSyncSaveOptions::SaveOptions(save_options) => { Some(save_options.include_text.unwrap_or(false)) } }, - // We do not have any save info. Kind affects didChange only. - lsp::TextDocumentSyncCapability::Kind(_) => None, } } @@ -13292,10 +12634,10 @@ fn ensure_uniform_list_compatible_label(label: &mut CodeLabel) { let mut offset_map = vec![0; label.text.len() + 1]; let mut last_char_was_space = false; let mut new_idx = 0; - let chars = label.text.char_indices().fuse(); + let mut chars = label.text.char_indices().fuse(); let mut newlines_removed = false; - for (idx, c) in chars { + while let Some((idx, c)) = chars.next() { offset_map[idx] = new_idx; match c { diff --git a/crates/project/src/lsp_store/clangd_ext.rs b/crates/project/src/lsp_store/clangd_ext.rs index b02f68dd4d..6a09bb99b4 100644 --- a/crates/project/src/lsp_store/clangd_ext.rs +++ b/crates/project/src/lsp_store/clangd_ext.rs @@ -1,14 +1,14 @@ -use std::{borrow::Cow, sync::Arc}; +use std::sync::Arc; use ::serde::{Deserialize, Serialize}; use gpui::WeakEntity; use language::{CachedLspAdapter, Diagnostic, DiagnosticSourceKind}; -use lsp::{LanguageServer, LanguageServerName}; +use lsp::LanguageServer; use util::ResultExt as _; -use crate::{LspStore, lsp_store::DocumentDiagnosticsUpdate}; +use crate::LspStore; -pub const CLANGD_SERVER_NAME: LanguageServerName = LanguageServerName::new_static("clangd"); +pub const CLANGD_SERVER_NAME: &str = "clangd"; const INACTIVE_REGION_MESSAGE: &str = "inactive region"; const INACTIVE_DIAGNOSTIC_SEVERITY: lsp::DiagnosticSeverity = lsp::DiagnosticSeverity::INFORMATION; @@ -34,7 +34,7 @@ pub fn is_inactive_region(diag: &Diagnostic) -> bool { && diag .source .as_ref() - .is_some_and(|v| v == &CLANGD_SERVER_NAME.0) + .is_some_and(|v| v == CLANGD_SERVER_NAME) } pub fn is_lsp_inactive_region(diag: &lsp::Diagnostic) -> bool { @@ -43,7 +43,7 @@ pub fn is_lsp_inactive_region(diag: &lsp::Diagnostic) -> bool { && diag .source .as_ref() - .is_some_and(|v| v == &CLANGD_SERVER_NAME.0) + .is_some_and(|v| v == CLANGD_SERVER_NAME) } pub fn register_notifications( @@ -51,14 +51,14 @@ pub fn register_notifications( language_server: &LanguageServer, adapter: Arc, ) { - if language_server.name() != CLANGD_SERVER_NAME { + if language_server.name().0 != CLANGD_SERVER_NAME { return; } let server_id = language_server.server_id(); language_server .on_notification::({ - let adapter = adapter; + let adapter = adapter.clone(); let this = lsp_store; move |params: InactiveRegionsParams, cx| { @@ -81,16 +81,12 @@ pub fn register_notifications( version: params.text_document.version, diagnostics, }; - this.merge_lsp_diagnostics( + this.merge_diagnostics( + server_id, + mapped_diagnostics, + None, DiagnosticSourceKind::Pushed, - vec![DocumentDiagnosticsUpdate { - server_id, - diagnostics: mapped_diagnostics, - result_id: None, - disk_based_sources: Cow::Borrowed( - &adapter.disk_based_diagnostic_sources, - ), - }], + &adapter.disk_based_diagnostic_sources, |_, diag, _| !is_inactive_region(diag), cx, ) diff --git a/crates/project/src/lsp_store/lsp_ext_command.rs b/crates/project/src/lsp_store/lsp_ext_command.rs index 1c969f8114..cb13fa5efc 100644 --- a/crates/project/src/lsp_store/lsp_ext_command.rs +++ b/crates/project/src/lsp_store/lsp_ext_command.rs @@ -115,14 +115,14 @@ impl LspCommand for ExpandMacro { message: Self::ProtoRequest, _: Entity, buffer: Entity, - cx: AsyncApp, + mut cx: AsyncApp, ) -> anyhow::Result { let position = message .position .and_then(deserialize_anchor) .context("invalid position")?; Ok(Self { - position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, + position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, }) } @@ -249,14 +249,14 @@ impl LspCommand for OpenDocs { message: Self::ProtoRequest, _: Entity, buffer: Entity, - cx: AsyncApp, + mut cx: AsyncApp, ) -> anyhow::Result { let position = message .position .and_then(deserialize_anchor) .context("invalid position")?; Ok(Self { - position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, + position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, }) } @@ -462,14 +462,14 @@ impl LspCommand for GoToParentModule { request: Self::ProtoRequest, _: Entity, buffer: Entity, - cx: AsyncApp, + mut cx: AsyncApp, ) -> anyhow::Result { let position = request .position .and_then(deserialize_anchor) .context("bad request with bad position")?; Ok(Self { - position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, + position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, }) } diff --git a/crates/project/src/lsp_store/rust_analyzer_ext.rs b/crates/project/src/lsp_store/rust_analyzer_ext.rs index 54f63220b1..d78715d385 100644 --- a/crates/project/src/lsp_store/rust_analyzer_ext.rs +++ b/crates/project/src/lsp_store/rust_analyzer_ext.rs @@ -1,13 +1,13 @@ use ::serde::{Deserialize, Serialize}; use anyhow::Context as _; -use gpui::{App, AsyncApp, Entity, Task, WeakEntity}; -use language::{Buffer, ServerHealth}; -use lsp::{LanguageServer, LanguageServerId, LanguageServerName}; +use gpui::{App, Entity, Task, WeakEntity}; +use language::ServerHealth; +use lsp::LanguageServer; use rpc::proto; use crate::{LspStore, LspStoreEvent, Project, ProjectPath, lsp_store}; -pub const RUST_ANALYZER_NAME: LanguageServerName = LanguageServerName::new_static("rust-analyzer"); +pub const RUST_ANALYZER_NAME: &str = "rust-analyzer"; pub const CARGO_DIAGNOSTICS_SOURCE_NAME: &str = "rustc"; /// Experimental: Informs the end user about the state of the server @@ -34,6 +34,7 @@ pub fn register_notifications(lsp_store: WeakEntity, language_server: language_server .on_notification::({ + let name = name.clone(); move |params, cx| { let message = params.message; let log_message = message.as_ref().map(|message| { @@ -83,32 +84,35 @@ pub fn register_notifications(lsp_store: WeakEntity, language_server: pub fn cancel_flycheck( project: Entity, - buffer_path: Option, + buffer_path: ProjectPath, cx: &mut App, ) -> Task> { let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client(); let lsp_store = project.read(cx).lsp_store(); - let buffer = buffer_path.map(|buffer_path| { - project.update(cx, |project, cx| { - project.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.open_buffer(buffer_path, cx) - }) + let buffer = project.update(cx, |project, cx| { + project.buffer_store().update(cx, |buffer_store, cx| { + buffer_store.open_buffer(buffer_path, cx) }) }); cx.spawn(async move |cx| { - let buffer = match buffer { - Some(buffer) => Some(buffer.await?), - None => None, - }; - let Some(rust_analyzer_server) = find_rust_analyzer_server(&project, buffer.as_ref(), cx) + let buffer = buffer.await?; + let Some(rust_analyzer_server) = project + .update(cx, |project, cx| { + buffer.update(cx, |buffer, cx| { + project.language_server_id_for_name(buffer, RUST_ANALYZER_NAME, cx) + }) + })? + .await else { return Ok(()); }; + let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id().to_proto())?; if let Some((client, project_id)) = upstream_client { let request = proto::LspExtCancelFlycheck { project_id, + buffer_id, language_server_id: rust_analyzer_server.to_proto(), }; client @@ -131,33 +135,32 @@ pub fn cancel_flycheck( pub fn run_flycheck( project: Entity, - buffer_path: Option, + buffer_path: ProjectPath, cx: &mut App, ) -> Task> { let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client(); let lsp_store = project.read(cx).lsp_store(); - let buffer = buffer_path.map(|buffer_path| { - project.update(cx, |project, cx| { - project.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.open_buffer(buffer_path, cx) - }) + let buffer = project.update(cx, |project, cx| { + project.buffer_store().update(cx, |buffer_store, cx| { + buffer_store.open_buffer(buffer_path, cx) }) }); cx.spawn(async move |cx| { - let buffer = match buffer { - Some(buffer) => Some(buffer.await?), - None => None, - }; - let Some(rust_analyzer_server) = find_rust_analyzer_server(&project, buffer.as_ref(), cx) + let buffer = buffer.await?; + let Some(rust_analyzer_server) = project + .update(cx, |project, cx| { + buffer.update(cx, |buffer, cx| { + project.language_server_id_for_name(buffer, RUST_ANALYZER_NAME, cx) + }) + })? + .await else { return Ok(()); }; + let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id().to_proto())?; if let Some((client, project_id)) = upstream_client { - let buffer_id = buffer - .map(|buffer| buffer.read_with(cx, |buffer, _| buffer.remote_id().to_proto())) - .transpose()?; let request = proto::LspExtRunFlycheck { project_id, buffer_id, @@ -188,32 +191,35 @@ pub fn run_flycheck( pub fn clear_flycheck( project: Entity, - buffer_path: Option, + buffer_path: ProjectPath, cx: &mut App, ) -> Task> { let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client(); let lsp_store = project.read(cx).lsp_store(); - let buffer = buffer_path.map(|buffer_path| { - project.update(cx, |project, cx| { - project.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.open_buffer(buffer_path, cx) - }) + let buffer = project.update(cx, |project, cx| { + project.buffer_store().update(cx, |buffer_store, cx| { + buffer_store.open_buffer(buffer_path, cx) }) }); cx.spawn(async move |cx| { - let buffer = match buffer { - Some(buffer) => Some(buffer.await?), - None => None, - }; - let Some(rust_analyzer_server) = find_rust_analyzer_server(&project, buffer.as_ref(), cx) + let buffer = buffer.await?; + let Some(rust_analyzer_server) = project + .update(cx, |project, cx| { + buffer.update(cx, |buffer, cx| { + project.language_server_id_for_name(buffer, RUST_ANALYZER_NAME, cx) + }) + })? + .await else { return Ok(()); }; + let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id().to_proto())?; if let Some((client, project_id)) = upstream_client { let request = proto::LspExtClearFlycheck { project_id, + buffer_id, language_server_id: rust_analyzer_server.to_proto(), }; client @@ -233,40 +239,3 @@ pub fn clear_flycheck( anyhow::Ok(()) }) } - -fn find_rust_analyzer_server( - project: &Entity, - buffer: Option<&Entity>, - cx: &mut AsyncApp, -) -> Option { - project - .read_with(cx, |project, cx| { - buffer - .and_then(|buffer| { - project.language_server_id_for_name(buffer.read(cx), &RUST_ANALYZER_NAME, cx) - }) - // If no rust-analyzer found for the current buffer (e.g. `settings.json`), fall back to the project lookup - // and use project's rust-analyzer if it's the only one. - .or_else(|| { - let rust_analyzer_servers = project - .lsp_store() - .read(cx) - .language_server_statuses - .iter() - .filter_map(|(server_id, server_status)| { - if server_status.name == RUST_ANALYZER_NAME { - Some(*server_id) - } else { - None - } - }) - .collect::>(); - if rust_analyzer_servers.len() == 1 { - rust_analyzer_servers.first().copied() - } else { - None - } - }) - }) - .ok()? -} diff --git a/crates/project/src/manifest_tree.rs b/crates/project/src/manifest_tree.rs index 5a3c7bd40f..7266acb5b4 100644 --- a/crates/project/src/manifest_tree.rs +++ b/crates/project/src/manifest_tree.rs @@ -7,12 +7,18 @@ mod manifest_store; mod path_trie; mod server_tree; -use std::{borrow::Borrow, collections::hash_map::Entry, ops::ControlFlow, path::Path, sync::Arc}; +use std::{ + borrow::Borrow, + collections::{BTreeMap, hash_map::Entry}, + ops::ControlFlow, + path::Path, + sync::Arc, +}; use collections::HashMap; -use gpui::{App, AppContext as _, Context, Entity, Subscription}; +use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Subscription}; use language::{ManifestDelegate, ManifestName, ManifestQuery}; -pub use manifest_store::ManifestProvidersStore; +pub use manifest_store::ManifestProviders; use path_trie::{LabelPresence, RootPathTrie, TriePath}; use settings::{SettingsStore, WorktreeId}; use worktree::{Event as WorktreeEvent, Snapshot, Worktree}; @@ -22,7 +28,9 @@ use crate::{ worktree_store::{WorktreeStore, WorktreeStoreEvent}, }; -pub(crate) use server_tree::{LanguageServerTree, LanguageServerTreeNode, LaunchDisposition}; +pub(crate) use server_tree::{ + AdapterQuery, LanguageServerTree, LanguageServerTreeNode, LaunchDisposition, +}; struct WorktreeRoots { roots: RootPathTrie, @@ -43,9 +51,12 @@ impl WorktreeRoots { match event { WorktreeEvent::UpdatedEntries(changes) => { for (path, _, kind) in changes.iter() { - if kind == &worktree::PathChange::Removed { - let path = TriePath::from(path.as_ref()); - this.roots.remove(&path); + match kind { + worktree::PathChange::Removed => { + let path = TriePath::from(path.as_ref()); + this.roots.remove(&path); + } + _ => {} } } } @@ -70,6 +81,14 @@ pub struct ManifestTree { _subscriptions: [Subscription; 2], } +#[derive(PartialEq)] +pub(crate) enum ManifestTreeEvent { + WorktreeRemoved(WorktreeId), + Cleared, +} + +impl EventEmitter for ManifestTree {} + impl ManifestTree { pub fn new(worktree_store: Entity, cx: &mut App) -> Entity { cx.new(|cx| Self { @@ -77,33 +96,35 @@ impl ManifestTree { _subscriptions: [ cx.subscribe(&worktree_store, Self::on_worktree_store_event), cx.observe_global::(|this, cx| { - for roots in this.root_points.values_mut() { + for (_, roots) in &mut this.root_points { roots.update(cx, |worktree_roots, _| { worktree_roots.roots = RootPathTrie::new(); }) } + cx.emit(ManifestTreeEvent::Cleared); }), ], worktree_store, }) } - pub(crate) fn root_for_path( &mut self, - ProjectPath { worktree_id, path }: &ProjectPath, - manifest_name: &ManifestName, - delegate: &Arc, + ProjectPath { worktree_id, path }: ProjectPath, + manifests: &mut dyn Iterator, + delegate: Arc, cx: &mut App, - ) -> Option { - debug_assert_eq!(delegate.worktree_id(), *worktree_id); - let (mut marked_path, mut current_presence) = (None, LabelPresence::KnownAbsent); - let worktree_roots = match self.root_points.entry(*worktree_id) { + ) -> BTreeMap { + debug_assert_eq!(delegate.worktree_id(), worktree_id); + let mut roots = BTreeMap::from_iter( + manifests.map(|manifest| (manifest, (None, LabelPresence::KnownAbsent))), + ); + let worktree_roots = match self.root_points.entry(worktree_id) { Entry::Occupied(occupied_entry) => occupied_entry.get().clone(), Entry::Vacant(vacant_entry) => { let Some(worktree) = self .worktree_store .read(cx) - .worktree_for_id(*worktree_id, cx) + .worktree_for_id(worktree_id, cx) else { return Default::default(); }; @@ -112,16 +133,16 @@ impl ManifestTree { } }; - let key = TriePath::from(&**path); + let key = TriePath::from(&*path); worktree_roots.read_with(cx, |this, _| { this.roots.walk(&key, &mut |path, labels| { for (label, presence) in labels { - if label == manifest_name { - if current_presence > *presence { + if let Some((marked_path, current_presence)) = roots.get_mut(label) { + if *current_presence > *presence { debug_assert!(false, "RootPathTrie precondition violation; while walking the tree label presence is only allowed to increase"); } - marked_path = Some(ProjectPath {worktree_id: *worktree_id, path: path.clone()}); - current_presence = *presence; + *marked_path = Some(ProjectPath {worktree_id, path: path.clone()}); + *current_presence = *presence; } } @@ -129,9 +150,12 @@ impl ManifestTree { }); }); - if current_presence == LabelPresence::KnownAbsent { - // Some part of the path is unexplored. - let depth = marked_path + for (manifest_name, (root_path, presence)) in &mut roots { + if *presence == LabelPresence::Present { + continue; + } + + let depth = root_path .as_ref() .map(|root_path| { path.strip_prefix(&root_path.path) @@ -141,10 +165,13 @@ impl ManifestTree { }) .unwrap_or_else(|| path.components().count() + 1); - if depth > 0 - && let Some(provider) = - ManifestProvidersStore::global(cx).get(manifest_name.borrow()) - { + if depth > 0 { + let Some(provider) = ManifestProviders::global(cx).get(manifest_name.borrow()) + else { + log::warn!("Manifest provider `{}` not found", manifest_name.as_ref()); + continue; + }; + let root = provider.search(ManifestQuery { path: path.clone(), depth, @@ -155,9 +182,9 @@ impl ManifestTree { let root = TriePath::from(&*known_root); this.roots .insert(&root, manifest_name.clone(), LabelPresence::Present); - current_presence = LabelPresence::Present; - marked_path = Some(ProjectPath { - worktree_id: *worktree_id, + *presence = LabelPresence::Present; + *root_path = Some(ProjectPath { + worktree_id, path: known_root, }); }), @@ -168,34 +195,27 @@ impl ManifestTree { } } } - marked_path.filter(|_| current_presence.eq(&LabelPresence::Present)) - } - pub(crate) fn root_for_path_or_worktree_root( - &mut self, - project_path: &ProjectPath, - manifest_name: Option<&ManifestName>, - delegate: &Arc, - cx: &mut App, - ) -> ProjectPath { - let worktree_id = project_path.worktree_id; - // Backwards-compat: Fill in any adapters for which we did not detect the root as having the project root at the root of a worktree. - manifest_name - .and_then(|manifest_name| self.root_for_path(project_path, manifest_name, delegate, cx)) - .unwrap_or_else(|| ProjectPath { - worktree_id, - path: Arc::from(Path::new("")), + roots + .into_iter() + .filter_map(|(k, (path, presence))| { + let path = path?; + presence.eq(&LabelPresence::Present).then(|| (k, path)) }) + .collect() } - fn on_worktree_store_event( &mut self, _: Entity, evt: &WorktreeStoreEvent, - _: &mut Context, + cx: &mut Context, ) { - if let WorktreeStoreEvent::WorktreeRemoved(_, worktree_id) = evt { - self.root_points.remove(worktree_id); + match evt { + WorktreeStoreEvent::WorktreeRemoved(_, worktree_id) => { + self.root_points.remove(&worktree_id); + cx.emit(ManifestTreeEvent::WorktreeRemoved(*worktree_id)); + } + _ => {} } } } @@ -203,7 +223,6 @@ impl ManifestTree { pub(crate) struct ManifestQueryDelegate { worktree: Snapshot, } - impl ManifestQueryDelegate { pub fn new(worktree: Snapshot) -> Self { Self { worktree } @@ -212,8 +231,10 @@ impl ManifestQueryDelegate { impl ManifestDelegate for ManifestQueryDelegate { fn exists(&self, path: &Path, is_dir: Option) -> bool { - self.worktree.entry_for_path(path).is_some_and(|entry| { - is_dir.is_none_or(|is_required_to_be_dir| is_required_to_be_dir == entry.is_dir()) + self.worktree.entry_for_path(path).map_or(false, |entry| { + is_dir.map_or(true, |is_required_to_be_dir| { + is_required_to_be_dir == entry.is_dir() + }) }) } diff --git a/crates/project/src/manifest_tree/manifest_store.rs b/crates/project/src/manifest_tree/manifest_store.rs index cf9f81aee4..0462b25798 100644 --- a/crates/project/src/manifest_tree/manifest_store.rs +++ b/crates/project/src/manifest_tree/manifest_store.rs @@ -1,4 +1,4 @@ -use collections::{HashMap, HashSet}; +use collections::HashMap; use gpui::{App, Global, SharedString}; use parking_lot::RwLock; use std::{ops::Deref, sync::Arc}; @@ -11,13 +11,13 @@ struct ManifestProvidersState { } #[derive(Clone, Default)] -pub struct ManifestProvidersStore(Arc>); +pub struct ManifestProviders(Arc>); #[derive(Default)] -struct GlobalManifestProvider(ManifestProvidersStore); +struct GlobalManifestProvider(ManifestProviders); impl Deref for GlobalManifestProvider { - type Target = ManifestProvidersStore; + type Target = ManifestProviders; fn deref(&self) -> &Self::Target { &self.0 @@ -26,7 +26,7 @@ impl Deref for GlobalManifestProvider { impl Global for GlobalManifestProvider {} -impl ManifestProvidersStore { +impl ManifestProviders { /// Returns the global [`ManifestStore`]. /// /// Inserts a default [`ManifestStore`] if one does not yet exist. @@ -45,7 +45,4 @@ impl ManifestProvidersStore { pub(super) fn get(&self, name: &SharedString) -> Option> { self.0.read().providers.get(name).cloned() } - pub(crate) fn manifest_file_names(&self) -> HashSet { - self.0.read().providers.keys().cloned().collect() - } } diff --git a/crates/project/src/manifest_tree/path_trie.rs b/crates/project/src/manifest_tree/path_trie.rs index 9cebfda25c..1a0736765a 100644 --- a/crates/project/src/manifest_tree/path_trie.rs +++ b/crates/project/src/manifest_tree/path_trie.rs @@ -22,9 +22,9 @@ pub(super) struct RootPathTrie